1 /*jslint browser: true, continue: true, white: true, forin: true*/
4 * Core MediaWiki JavaScript Library
7 var mw = ( function ( $, undefined ) {
12 var hasOwn = Object.prototype.hasOwnProperty;
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 = $.makeArray( 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 ? [] : $.makeArray( 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.
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
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.
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 = $.makeArray( arguments );
306 return new Message( mw.messages, key, parameters );
310 * Gets a message string, similar to wfMsg()
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 // Flag indicating that document ready has occured
373 // Selector cache for the marker element. Use getMarker() to get/use the marker!
376 /* Cache document ready status */
378 $(document).ready( function () {
382 /* Private methods */
384 function getMarker() {
390 $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
391 if ( $marker.length ) {
394 mw.log( 'getMarker> No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically.' );
395 $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' );
401 * Create a new style tag and add it to the DOM.
403 * @param text String: CSS text
404 * @param $nextnode mixed: [optional] An Element or jQuery object for an element where
405 * the style tag should be inserted before. Otherwise appended to the <head>.
406 * @return HTMLStyleElement
408 function addStyleTag( text, $nextnode ) {
409 var s = document.createElement( 'style' );
411 s.rel = 'stylesheet';
412 // Insert into document before setting cssText (bug 33305)
414 // If a raw element, create a jQuery object, otherwise use directly
415 if ( $nextnode.nodeType ) {
416 $nextnode = $( $nextnode );
418 $nextnode.before( s );
420 document.getElementsByTagName('head')[0].appendChild( s );
422 if ( s.styleSheet ) {
423 s.styleSheet.cssText = text; // IE
425 // Safari sometimes borks on null
426 s.appendChild( document.createTextNode( String( text ) ) );
431 function addInlineCSS( css ) {
432 var $style, style, $newStyle;
433 $style = getMarker().prev();
434 // Disable <style> tag recycling/concatenation because of bug 34669
435 if ( false && $style.is( 'style' ) && $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) {
436 // There's already a dynamic <style> tag present, append to it. This recycling of
437 // <style> tags is for bug 31676 (can't have more than 32 <style> tags in IE)
438 style = $style.get( 0 );
439 if ( style.styleSheet ) {
440 style.styleSheet.cssText += css; // IE
442 style.appendChild( document.createTextNode( String( css ) ) );
445 $newStyle = $( addStyleTag( css, getMarker() ) )
446 .data( 'ResourceLoaderDynamicStyleTag', true );
450 function compare( a, b ) {
452 if ( a.length !== b.length ) {
455 for ( i = 0; i < b.length; i += 1 ) {
456 if ( $.isArray( a[i] ) ) {
457 if ( !compare( a[i], b[i] ) ) {
461 if ( a[i] !== b[i] ) {
469 * Generates an ISO8601 "basic" string from a UNIX timestamp
471 function formatVersionNumber( timestamp ) {
472 var pad = function ( a, b, c ) {
473 return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
476 d.setTime( timestamp * 1000 );
478 pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T',
479 pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z'
484 * Resolves dependencies and detects circular references.
486 * @param module String Name of the top-level module whose dependencies shall be
487 * resolved and sorted.
488 * @param resolved Array Returns a topological sort of the given module and its
489 * dependencies, such that later modules depend on earlier modules. The array
490 * contains the module names. If the array contains already some module names,
491 * this function appends its result to the pre-existing array.
492 * @param unresolved Object [optional] Hash used to track the current dependency
493 * chain; used to report loops in the dependency graph.
494 * @throws Error if any unregistered module or a dependency loop is encountered
496 function sortDependencies( module, resolved, unresolved ) {
499 if ( registry[module] === undefined ) {
500 throw new Error( 'Unknown dependency: ' + module );
502 // Resolves dynamic loader function and replaces it with its own results
503 if ( $.isFunction( registry[module].dependencies ) ) {
504 registry[module].dependencies = registry[module].dependencies();
505 // Ensures the module's dependencies are always in an array
506 if ( typeof registry[module].dependencies !== 'object' ) {
507 registry[module].dependencies = [registry[module].dependencies];
510 if ( $.inArray( module, resolved ) !== -1 ) {
511 // Module already resolved; nothing to do.
514 // unresolved is optional, supply it if not passed in
518 // Tracks down dependencies
519 deps = registry[module].dependencies;
521 for ( n = 0; n < len; n += 1 ) {
522 if ( $.inArray( deps[n], resolved ) === -1 ) {
523 if ( unresolved[deps[n]] ) {
525 'Circular reference detected: ' + module +
531 unresolved[module] = true;
532 sortDependencies( deps[n], resolved, unresolved );
533 delete unresolved[module];
536 resolved[resolved.length] = module;
540 * Gets a list of module names that a module depends on in their proper dependency
543 * @param module string module name or array of string module names
544 * @return list of dependencies, including 'module'.
545 * @throws Error if circular reference is detected
547 function resolve( module ) {
550 // Allow calling with an array of module names
551 if ( $.isArray( module ) ) {
553 for ( m = 0; m < module.length; m += 1 ) {
554 sortDependencies( module[m], resolved );
559 if ( typeof module === 'string' ) {
561 sortDependencies( module, resolved );
565 throw new Error( 'Invalid module argument: ' + module );
569 * Narrows a list of module names down to those matching a specific
570 * state (see comment on top of this scope for a list of valid states).
571 * One can also filter for 'unregistered', which will return the
572 * modules names that don't have a registry entry.
574 * @param states string or array of strings of module states to filter by
575 * @param modules array list of module names to filter (optional, by default the entire
577 * @return array list of filtered module names
579 function filter( states, modules ) {
580 var list, module, s, m;
582 // Allow states to be given as a string
583 if ( typeof states === 'string' ) {
586 // If called without a list of modules, build and use a list of all modules
588 if ( modules === undefined ) {
590 for ( module in registry ) {
591 modules[modules.length] = module;
594 // Build a list of modules which are in one of the specified states
595 for ( s = 0; s < states.length; s += 1 ) {
596 for ( m = 0; m < modules.length; m += 1 ) {
597 if ( registry[modules[m]] === undefined ) {
598 // Module does not exist
599 if ( states[s] === 'unregistered' ) {
601 list[list.length] = modules[m];
604 // Module exists, check state
605 if ( registry[modules[m]].state === states[s] ) {
607 list[list.length] = modules[m];
616 * Determine whether all dependencies are in state 'ready', which means we may
617 * execute the module or job now.
619 * @param dependencies Array dependencies (module names) to be checked.
621 * @return Boolean true if all dependencies are in state 'ready', false otherwise
623 function allReady( dependencies ) {
624 return filter( 'ready', dependencies ).length === dependencies.length;
628 * Log a message to window.console, if possible. Useful to force logging of some
629 * errors that are otherwise hard to detect, even if mw.log is not available. (I.e.,
630 * this logs also if not in debug mode.)
632 * @param msg String text for the log entry
633 * @param e Error [optional] to also log.
635 function log( msg, e ) {
636 if ( window.console && typeof window.console.log === 'function' ) {
645 * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs
646 * and modules that depend upon this module. if the given module failed, propagate the 'error'
647 * state up the dependency tree; otherwise, execute all jobs/modules that now have all their
648 * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any.
650 * @param module String name of module that entered one of the states 'ready', 'error', or 'missing'.
652 function handlePending( module ) {
653 var j, job, hasErrors, m, stateChange;
656 if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) {
657 // If the current module failed, mark all dependent modules also as failed.
658 // Iterate until steady-state to propagate the error state upwards in the
662 for ( m in registry ) {
663 if ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) {
664 if ( filter( ['error', 'missing'], registry[m].dependencies ).length > 0 ) {
665 registry[m].state = 'error';
670 } while ( stateChange );
673 // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
674 for ( j = 0; j < jobs.length; j += 1 ) {
675 hasErrors = filter( ['error', 'missing'], jobs[j].dependencies ).length > 0;
676 if ( hasErrors || allReady( jobs[j].dependencies ) ) {
677 // All dependencies satisfied, or some have errors
683 throw new Error ("Module " + module + " failed.");
685 if ( $.isFunction( job.ready ) ) {
690 if ( $.isFunction( job.error ) ) {
692 job.error( e, [module] );
694 // A user-defined operation raised an exception. Swallow to protect
695 // our state machine!
696 log( 'mw.loader::handlePending> Exception thrown by job.error()', ex );
703 if ( registry[module].state === 'ready' ) {
704 // The current module became 'ready'. Recursively execute all dependent modules that are loaded
705 // and now have all dependencies satisfied.
706 for ( m in registry ) {
707 if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) {
715 * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
716 * depending on whether document-ready has occurred yet and whether we are in async mode.
718 * @param src String: URL to script, will be used as the src attribute in the script tag
719 * @param callback Function: Optional callback which will be run when the script is done
721 function addScript( src, callback, async ) {
722 var done = false, script, head;
723 if ( ready || async ) {
724 // jQuery's getScript method is NOT better than doing this the old-fashioned way
725 // because jQuery will eval the script's code, and errors will not have sane
727 script = document.createElement( 'script' );
728 script.setAttribute( 'src', src );
729 script.setAttribute( 'type', 'text/javascript' );
730 if ( $.isFunction( callback ) ) {
731 // Attach handlers for all browsers (based on jQuery.ajax)
732 script.onload = script.onreadystatechange = function() {
738 || /loaded|complete/.test( script.readyState )
746 // Handle memory leak in IE. This seems to fail in
747 // IE7 sometimes (Permission Denied error when
748 // accessing script.parentNode) so wrap it in
751 script.onload = script.onreadystatechange = null;
752 if ( script.parentNode ) {
753 script.parentNode.removeChild( script );
756 // Dereference the script
763 if ( window.opera ) {
764 // Appending to the <head> blocks rendering completely in Opera,
765 // so append to the <body> after document ready. This means the
766 // scripts only start loading after the document has been rendered,
767 // but so be it. Opera users don't deserve faster web pages if their
768 // browser makes it impossible
769 $( function() { document.body.appendChild( script ); } );
771 // IE-safe way of getting the <head> . document.documentElement.head doesn't
772 // work in scripts that run in the <head>
773 head = document.getElementsByTagName( 'head' )[0];
774 ( document.body || head ).appendChild( script );
777 document.write( mw.html.element(
778 'script', { 'type': 'text/javascript', 'src': src }, ''
780 if ( $.isFunction( callback ) ) {
781 // Document.write is synchronous, so this is called when it's done
782 // FIXME: that's a lie. doc.write isn't actually synchronous
789 * Executes a loaded module, making it ready to use
791 * @param module string module name to execute
793 function execute( module ) {
794 var style, media, i, script, markModuleReady, nestedAddScript;
796 if ( registry[module] === undefined ) {
797 throw new Error( 'Module has not been registered yet: ' + module );
798 } else if ( registry[module].state === 'registered' ) {
799 throw new Error( 'Module has not been requested from the server yet: ' + module );
800 } else if ( registry[module].state === 'loading' ) {
801 throw new Error( 'Module has not completed loading yet: ' + module );
802 } else if ( registry[module].state === 'ready' ) {
803 throw new Error( 'Module has already been loaded: ' + module );
807 if ( $.isPlainObject( registry[module].style ) ) {
808 // 'media' type ignored, see documentation of mw.loader.implement
809 for ( media in registry[module].style ) {
810 style = registry[module].style[media];
811 if ( $.isArray( style ) ) {
812 for ( i = 0; i < style.length; i += 1 ) {
813 getMarker().before( mw.html.element( 'link', {
819 } else if ( typeof style === 'string' ) {
820 addInlineCSS( style );
824 // Add localizations to message system
825 if ( $.isPlainObject( registry[module].messages ) ) {
826 mw.messages.set( registry[module].messages );
830 script = registry[module].script;
831 markModuleReady = function() {
832 registry[module].state = 'ready';
833 handlePending( module );
835 nestedAddScript = function ( arr, callback, async, i ) {
836 // Recursively call addScript() in its own callback
837 // for each element of arr.
838 if ( i >= arr.length ) {
839 // We're at the end of the array
844 addScript( arr[i], function() {
845 nestedAddScript( arr, callback, async, i + 1 );
849 if ( $.isArray( script ) ) {
850 registry[module].state = 'loading';
851 nestedAddScript( script, markModuleReady, registry[module].async, 0 );
852 } else if ( $.isFunction( script ) ) {
857 // This needs to NOT use mw.log because these errors are common in production mode
858 // and not in debug mode, such as when a symbol that should be global isn't exported
859 log('mw.loader::execute> Exception thrown by ' + module + ': ' + e.message, e);
860 registry[module].state = 'error';
861 handlePending( module );
866 * Adds a dependencies to the queue with optional callbacks to be run
867 * when the dependencies are ready or fail
869 * @param dependencies string module name or array of string module names
870 * @param ready function callback to execute when all dependencies are ready
871 * @param error function callback to execute when any dependency fails
872 * @param async (optional) If true, load modules asynchronously even if
873 * document ready has not yet occurred
875 function request( dependencies, ready, error, async ) {
876 var regItemDeps, regItemDepLen, n;
878 // Allow calling by single module name
879 if ( typeof dependencies === 'string' ) {
880 dependencies = [dependencies];
881 if ( registry[dependencies[0]] !== undefined ) {
882 // Cache repetitively accessed deep level object member
883 regItemDeps = registry[dependencies[0]].dependencies;
884 // Cache to avoid looped access to length property
885 regItemDepLen = regItemDeps.length;
886 for ( n = 0; n < regItemDepLen; n += 1 ) {
887 dependencies[dependencies.length] = regItemDeps[n];
892 // Add ready and error callbacks if they were given
893 if ( ready !== undefined || error !== undefined ) {
894 jobs[jobs.length] = {
895 'dependencies': filter(
896 ['registered', 'loading', 'loaded'],
904 // Queue up any dependencies that are registered
905 dependencies = filter( ['registered'], dependencies );
906 for ( n = 0; n < dependencies.length; n += 1 ) {
907 if ( $.inArray( dependencies[n], queue ) === -1 ) {
908 queue[queue.length] = dependencies[n];
910 // Mark this module as async in the registry
911 registry[dependencies[n]].async = true;
920 function sortQuery(o) {
921 var sorted = {}, key, a = [];
923 if ( hasOwn.call( o, key ) ) {
928 for ( key = 0; key < a.length; key += 1 ) {
929 sorted[a[key]] = o[a[key]];
935 * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
936 * to a query string of the form foo.bar,baz|bar.baz,quux
938 function buildModulesString( moduleMap ) {
939 var arr = [], p, prefix;
940 for ( prefix in moduleMap ) {
941 p = prefix === '' ? '' : prefix + '.';
942 arr.push( p + moduleMap[prefix].join( ',' ) );
944 return arr.join( '|' );
948 * Asynchronously append a script tag to the end of the body
949 * that invokes load.php
950 * @param moduleMap {Object}: Module map, see buildModulesString()
951 * @param currReqBase {Object}: Object with other parameters (other than 'modules') to use in the request
952 * @param sourceLoadScript {String}: URL of load.php
953 * @param async {Boolean}: If true, use an asynchrounous request even if document ready has not yet occurred
955 function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) {
956 var request = $.extend(
957 { 'modules': buildModulesString( moduleMap ) },
960 request = sortQuery( request );
961 // Asynchronously append a script tag to the end of the body
962 // Append &* to avoid triggering the IE6 extension check
963 addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
968 addStyleTag: addStyleTag,
971 * Requests dependencies from server, loading and executing when things when ready.
974 var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
975 source, group, g, i, modules, maxVersion, sourceLoadScript,
976 currReqBase, currReqBaseLength, moduleMap, l,
977 lastDotIndex, prefix, suffix, bytesAdded, async;
979 // Build a list of request parameters common to all requests.
981 skin: mw.config.get( 'skin' ),
982 lang: mw.config.get( 'wgUserLanguage' ),
983 debug: mw.config.get( 'debug' )
985 // Split module batch by source and by group.
987 maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 );
989 // Appends a list of modules from the queue to the batch
990 for ( q = 0; q < queue.length; q += 1 ) {
991 // Only request modules which are registered
992 if ( registry[queue[q]] !== undefined && registry[queue[q]].state === 'registered' ) {
993 // Prevent duplicate entries
994 if ( $.inArray( queue[q], batch ) === -1 ) {
995 batch[batch.length] = queue[q];
996 // Mark registered modules as loading
997 registry[queue[q]].state = 'loading';
1001 // Early exit if there's nothing to load...
1002 if ( !batch.length ) {
1006 // The queue has been processed into the batch, clear up the queue.
1009 // Always order modules alphabetically to help reduce cache
1010 // misses for otherwise identical content.
1013 // Split batch by source and by group.
1014 for ( b = 0; b < batch.length; b += 1 ) {
1015 bSource = registry[batch[b]].source;
1016 bGroup = registry[batch[b]].group;
1017 if ( splits[bSource] === undefined ) {
1018 splits[bSource] = {};
1020 if ( splits[bSource][bGroup] === undefined ) {
1021 splits[bSource][bGroup] = [];
1023 bSourceGroup = splits[bSource][bGroup];
1024 bSourceGroup[bSourceGroup.length] = batch[b];
1027 // Clear the batch - this MUST happen before we append any
1028 // script elements to the body or it's possible that a script
1029 // will be locally cached, instantly load, and work the batch
1030 // again, all before we've cleared it causing each request to
1031 // include modules which are already loaded.
1034 for ( source in splits ) {
1036 sourceLoadScript = sources[source].loadScript;
1038 for ( group in splits[source] ) {
1040 // Cache access to currently selected list of
1041 // modules for this group from this source.
1042 modules = splits[source][group];
1044 // Calculate the highest timestamp
1046 for ( g = 0; g < modules.length; g += 1 ) {
1047 if ( registry[modules[g]].version > maxVersion ) {
1048 maxVersion = registry[modules[g]].version;
1052 currReqBase = $.extend( { 'version': formatVersionNumber( maxVersion ) }, reqBase );
1053 // For user modules append a user name to the request.
1054 if ( group === "user" && mw.config.get( 'wgUserName' ) !== null ) {
1055 currReqBase.user = mw.config.get( 'wgUserName' );
1057 currReqBaseLength = $.param( currReqBase ).length;
1059 // We may need to split up the request to honor the query string length limit,
1060 // so build it piece by piece.
1061 l = currReqBaseLength + 9; // '&modules='.length == 9
1063 moduleMap = {}; // { prefix: [ suffixes ] }
1065 for ( i = 0; i < modules.length; i += 1 ) {
1066 // Determine how many bytes this module would add to the query string
1067 lastDotIndex = modules[i].lastIndexOf( '.' );
1068 // Note that these substr() calls work even if lastDotIndex == -1
1069 prefix = modules[i].substr( 0, lastDotIndex );
1070 suffix = modules[i].substr( lastDotIndex + 1 );
1071 bytesAdded = moduleMap[prefix] !== undefined
1072 ? suffix.length + 3 // '%2C'.length == 3
1073 : modules[i].length + 3; // '%7C'.length == 3
1075 // If the request would become too long, create a new one,
1076 // but don't create empty requests
1077 if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
1078 // This request would become too long, create a new one
1079 // and fire off the old one
1080 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1083 l = currReqBaseLength + 9;
1085 if ( moduleMap[prefix] === undefined ) {
1086 moduleMap[prefix] = [];
1088 moduleMap[prefix].push( suffix );
1089 if ( !registry[modules[i]].async ) {
1090 // If this module is blocking, make the entire request blocking
1091 // This is slightly suboptimal, but in practice mixing of blocking
1092 // and async modules will only occur in debug mode.
1097 // If there's anything left in moduleMap, request that too
1098 if ( !$.isEmptyObject( moduleMap ) ) {
1099 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1106 * Register a source.
1108 * @param id {String}: Short lowercase a-Z string representing a source, only used internally.
1109 * @param props {Object}: Object containing only the loadScript property which is a url to
1110 * the load.php location of the source.
1113 addSource: function ( id, props ) {
1115 // Allow multiple additions
1116 if ( typeof id === 'object' ) {
1117 for ( source in id ) {
1118 mw.loader.addSource( source, id[source] );
1123 if ( sources[id] !== undefined ) {
1124 throw new Error( 'source already registered: ' + id );
1127 sources[id] = props;
1133 * Registers a module, letting the system know about it and its
1134 * properties. Startup modules contain calls to this function.
1136 * @param module {String}: Module name
1137 * @param version {Number}: Module version number as a timestamp (falls backs to 0)
1138 * @param dependencies {String|Array|Function}: One string or array of strings of module
1139 * names on which this module depends, or a function that returns that array.
1140 * @param group {String}: Group which the module is in (optional, defaults to null)
1141 * @param source {String}: Name of the source. Defaults to local.
1143 register: function ( module, version, dependencies, group, source ) {
1145 // Allow multiple registration
1146 if ( typeof module === 'object' ) {
1147 for ( m = 0; m < module.length; m += 1 ) {
1148 // module is an array of module names
1149 if ( typeof module[m] === 'string' ) {
1150 mw.loader.register( module[m] );
1151 // module is an array of arrays
1152 } else if ( typeof module[m] === 'object' ) {
1153 mw.loader.register.apply( mw.loader, module[m] );
1159 if ( typeof module !== 'string' ) {
1160 throw new Error( 'module must be a string, not a ' + typeof module );
1162 if ( registry[module] !== undefined ) {
1163 throw new Error( 'module already registered: ' + module );
1165 // List the module as registered
1166 registry[module] = {
1167 'version': version !== undefined ? parseInt( version, 10 ) : 0,
1169 'group': typeof group === 'string' ? group : null,
1170 'source': typeof source === 'string' ? source: 'local',
1171 'state': 'registered'
1173 if ( typeof dependencies === 'string' ) {
1174 // Allow dependencies to be given as a single module name
1175 registry[module].dependencies = [dependencies];
1176 } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
1177 // Allow dependencies to be given as an array of module names
1178 // or a function which returns an array
1179 registry[module].dependencies = dependencies;
1184 * Implements a module, giving the system a course of action to take
1185 * upon loading. Results of a request for one or more modules contain
1186 * calls to this function.
1188 * All arguments are required.
1190 * @param module String: Name of module
1191 * @param script Mixed: Function of module code or String of URL to be used as the src
1192 * attribute when adding a script element to the body
1193 * @param style Object: Object of CSS strings keyed by media-type or Object of lists of URLs
1194 * keyed by media-type. Media-type should be "all" or "", actual types are not supported
1195 * right now due to the way execute() processes the stylesheets (they are concatenated
1196 * into a single <style> tag). In the past these weren't concatenated together (which is
1197 * these are keyed by media-type), but bug 31676 forces us to. In practice this is not a
1198 * problem because ResourceLoader only generates stylesheets for media-type all (e.g. print
1199 * stylesheets are wrapped in @media print {} and concatenated with the others).
1200 * @param msgs Object: List of key/value pairs to be passed through mw.messages.set
1202 implement: function ( module, script, style, msgs ) {
1204 if ( typeof module !== 'string' ) {
1205 throw new Error( 'module must be a string, not a ' + typeof module );
1207 if ( !$.isFunction( script ) && !$.isArray( script ) ) {
1208 throw new Error( 'script must be a function or an array, not a ' + typeof script );
1210 if ( !$.isPlainObject( style ) ) {
1211 throw new Error( 'style must be an object, not a ' + typeof style );
1213 if ( !$.isPlainObject( msgs ) ) {
1214 throw new Error( 'msgs must be an object, not a ' + typeof msgs );
1216 // Automatically register module
1217 if ( registry[module] === undefined ) {
1218 mw.loader.register( module );
1220 // Check for duplicate implementation
1221 if ( registry[module] !== undefined && registry[module].script !== undefined ) {
1222 throw new Error( 'module already implemented: ' + module );
1224 // Attach components
1225 registry[module].script = script;
1226 registry[module].style = style;
1227 registry[module].messages = msgs;
1228 // The module may already have been marked as erroneous
1229 if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
1230 registry[module].state = 'loaded';
1231 if ( allReady( registry[module].dependencies ) ) {
1238 * Executes a function as soon as one or more required modules are ready
1240 * @param dependencies {String|Array} Module name or array of modules names the callback
1241 * dependends on to be ready before executing
1242 * @param ready {Function} callback to execute when all dependencies are ready (optional)
1243 * @param error {Function} callback to execute when if dependencies have a errors (optional)
1245 using: function ( dependencies, ready, error ) {
1246 var tod = typeof dependencies;
1248 if ( tod !== 'object' && tod !== 'string' ) {
1249 throw new Error( 'dependencies must be a string or an array, not a ' + tod );
1251 // Allow calling with a single dependency as a string
1252 if ( tod === 'string' ) {
1253 dependencies = [dependencies];
1255 // Resolve entire dependency map
1256 dependencies = resolve( dependencies );
1257 if ( allReady( dependencies ) ) {
1258 // Run ready immediately
1259 if ( $.isFunction( ready ) ) {
1262 } else if ( filter( ['error', 'missing'], dependencies ).length ) {
1263 // Execute error immediately if any dependencies have errors
1264 if ( $.isFunction( error ) ) {
1265 error( new Error( 'one or more dependencies have state "error" or "missing"' ),
1269 // Not all dependencies are ready: queue up a request
1270 request( dependencies, ready, error );
1275 * Loads an external script or one or more modules for future use
1277 * @param modules {mixed} Either the name of a module, array of modules,
1278 * or a URL of an external script or style
1279 * @param type {String} mime-type to use if calling with a URL of an
1280 * external script or style; acceptable values are "text/css" and
1281 * "text/javascript"; if no type is provided, text/javascript is assumed.
1282 * @param async {Boolean} (optional) If true, load modules asynchronously
1283 * even if document ready has not yet occurred. If false (default),
1284 * block before document ready and load async after. If not set, true will
1285 * be assumed if loading a URL, and false will be assumed otherwise.
1287 load: function ( modules, type, async ) {
1288 var filtered, m, module;
1291 if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
1292 throw new Error( 'modules must be a string or an array, not a ' + typeof modules );
1294 // Allow calling with an external url or single dependency as a string
1295 if ( typeof modules === 'string' ) {
1296 // Support adding arbitrary external scripts
1297 if ( /^(https?:)?\/\//.test( modules ) ) {
1298 if ( async === undefined ) {
1299 // Assume async for bug 34542
1302 if ( type === 'text/css' ) {
1303 $( 'head' ).append( $( '<link>', {
1310 if ( type === 'text/javascript' || type === undefined ) {
1311 addScript( modules, null, async );
1315 throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
1317 // Called with single module
1318 modules = [modules];
1321 // Filter out undefined modules, otherwise resolve() will throw
1322 // an exception for trying to load an undefined module.
1323 // Undefined modules are acceptable here in load(), because load() takes
1324 // an array of unrelated modules, whereas the modules passed to
1325 // using() are related and must all be loaded.
1326 for ( filtered = [], m = 0; m < modules.length; m += 1 ) {
1327 module = registry[modules[m]];
1328 if ( module !== undefined ) {
1329 if ( $.inArray( module.state, ['error', 'missing'] ) === -1 ) {
1330 filtered[filtered.length] = modules[m];
1335 if (filtered.length === 0) {
1338 // Resolve entire dependency map
1339 filtered = resolve( filtered );
1340 // If all modules are ready, nothing to be done
1341 if ( allReady( filtered ) ) {
1344 // If any modules have errors: also quit.
1345 if ( filter( ['error', 'missing'], filtered ).length ) {
1348 // Since some modules are not yet ready, queue up a request.
1349 request( filtered, null, null, async );
1353 * Changes the state of a module
1355 * @param module {String|Object} module name or object of module name/state pairs
1356 * @param state {String} state name
1358 state: function ( module, state ) {
1361 if ( typeof module === 'object' ) {
1362 for ( m in module ) {
1363 mw.loader.state( m, module[m] );
1367 if ( registry[module] === undefined ) {
1368 mw.loader.register( module );
1370 if ( $.inArray(state, ['ready', 'error', 'missing']) !== -1
1371 && registry[module].state !== state ) {
1372 // Make sure pending modules depending on this one get executed if their
1373 // dependencies are now fulfilled!
1374 registry[module].state = state;
1375 handlePending( module );
1377 registry[module].state = state;
1382 * Gets the version of a module
1384 * @param module string name of module to get version for
1386 getVersion: function ( module ) {
1387 if ( registry[module] !== undefined && registry[module].version !== undefined ) {
1388 return formatVersionNumber( registry[module].version );
1394 * @deprecated since 1.18 use mw.loader.getVersion() instead
1396 version: function () {
1397 return mw.loader.getVersion.apply( mw.loader, arguments );
1401 * Gets the state of a module
1403 * @param module string name of module to get state for
1405 getState: function ( module ) {
1406 if ( registry[module] !== undefined && registry[module].state !== undefined ) {
1407 return registry[module].state;
1413 * Get names of all registered modules.
1417 getModuleNames: function () {
1418 return $.map( registry, function ( i, key ) {
1424 * For backwards-compatibility with Squid-cached pages. Loads mw.user
1427 mw.loader.load( 'mediawiki.user' );
1432 /** HTML construction helper functions */
1433 html: ( function () {
1434 function escapeCallback( s ) {
1451 * Escape a string for HTML. Converts special characters to HTML entities.
1452 * @param s The string to escape
1454 escape: function ( s ) {
1455 return s.replace( /['"<>&]/g, escapeCallback );
1459 * Wrapper object for raw HTML passed to mw.html.element().
1462 Raw: function ( value ) {
1467 * Wrapper object for CDATA element contents passed to mw.html.element()
1470 Cdata: function ( value ) {
1475 * Create an HTML element string, with safe escaping.
1477 * @param name The tag name.
1478 * @param attrs An object with members mapping element names to values
1479 * @param contents The contents of the element. May be either:
1480 * - string: The string is escaped.
1481 * - null or undefined: The short closing form is used, e.g. <br/>.
1482 * - this.Raw: The value attribute is included without escaping.
1483 * - this.Cdata: The value attribute is included, and an exception is
1484 * thrown if it contains an illegal ETAGO delimiter.
1485 * See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2
1489 * return h.element( 'div', {},
1490 * new h.Raw( h.element( 'img', {src: '<'} ) ) );
1491 * Returns <div><img src="<"/></div>
1493 element: function ( name, attrs, contents ) {
1494 var v, attrName, s = '<' + name;
1496 for ( attrName in attrs ) {
1497 v = attrs[attrName];
1498 // Convert name=true, to name=name
1502 } else if ( v === false ) {
1505 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
1507 if ( contents === undefined || contents === null ) {
1514 switch ( typeof contents ) {
1517 s += this.escape( contents );
1521 // Convert to string
1522 s += String( contents );
1525 if ( contents instanceof this.Raw ) {
1526 // Raw HTML inclusion
1527 s += contents.value;
1528 } else if ( contents instanceof this.Cdata ) {
1530 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
1531 throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
1533 s += contents.value;
1535 throw new Error( 'mw.html.element: Invalid type of contents' );
1538 s += '</' + name + '>';
1544 // Skeleton user object. mediawiki.user.js extends this
1553 // Alias $j to jQuery for backwards compatibility
1556 // Attach to window and globally alias
1557 window.mw = window.mediaWiki = mw;
1559 // Auto-register from pre-loaded startup scripts
1560 if ( jQuery.isFunction( window.startUp ) ) {
1562 window.startUp = undefined;