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 placeholder for {@link mw.log}
274 var log = function () {};
275 log.warn = function () {};
276 log.deprecate = function ( obj, key, val ) {
282 // Make the Map constructor publicly available.
285 // Make the Message constructor publicly available.
289 * List of configuration values
291 * Dummy placeholder. Initiated in startUp module as a new instance of mw.Map().
292 * If `$wgLegacyJavaScriptGlobals` is true, this Map will have its values
293 * in the global window object.
299 * Empty object that plugins can be installed in.
310 * Localization system
318 * Gets a message object, similar to wfMessage().
320 * @param {string} key Key of message to get
321 * @param {Mixed...} parameters Parameters for the $N replacements in messages.
322 * @return {mw.Message}
324 message: function ( key ) {
325 // Variadic arguments
326 var parameters = slice.call( arguments, 1 );
327 return new Message( mw.messages, key, parameters );
331 * Gets a message string, similar to wfMessage()
333 * @see mw.Message#toString
334 * @param {string} key Key of message to get
335 * @param {Mixed...} parameters Parameters for the $N replacements in messages.
338 msg: function ( /* key, parameters... */ ) {
339 return mw.message.apply( mw.message, arguments ).toString();
343 * Client-side module loader which integrates with the MediaWiki ResourceLoader
347 loader: ( function () {
349 /* Private Members */
352 * Mapping of registered modules
354 * The jquery module is pre-registered, because it must have already
355 * been provided for this object to have been built, and in debug mode
356 * jquery would have been provided through a unique loader request,
357 * making it impossible to hold back registration of jquery until after
360 * For exact details on support for script, style and messages, look at
361 * mw.loader.implement.
366 * 'version': ############## (unix timestamp),
367 * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
368 * 'group': 'somegroup', (or) null,
369 * 'source': 'local', 'someforeignwiki', (or) null
370 * 'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing'
373 * 'messages': { 'key': 'value' },
382 // Mapping of sources, keyed by source-id, values are objects.
386 // 'loadScript': 'http://foo.bar/w/load.php'
391 // List of modules which will be loaded as when ready
393 // List of modules to be loaded
395 // List of callback functions waiting for modules to be ready to be called
397 // Selector cache for the marker element. Use getMarker() to get/use the marker!
399 // Buffer for addEmbeddedCSS.
401 // Callbacks for addEmbeddedCSS.
402 cssCallbacks = $.Callbacks();
404 /* Private methods */
406 function getMarker() {
412 $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
413 if ( $marker.length ) {
416 mw.log( 'getMarker> No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically.' );
417 $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' );
423 * Create a new style tag and add it to the DOM.
426 * @param {string} text CSS text
427 * @param {Mixed} [nextnode] An Element or jQuery object for an element where
428 * the style tag should be inserted before. Otherwise appended to the `<head>`.
429 * @return {HTMLElement} Node reference to the created `<style>` tag.
431 function addStyleTag( text, nextnode ) {
432 var s = document.createElement( 'style' );
433 // Insert into document before setting cssText (bug 33305)
435 // Must be inserted with native insertBefore, not $.fn.before.
436 // When using jQuery to insert it, like $nextnode.before( s ),
437 // then IE6 will throw "Access is denied" when trying to append
438 // to .cssText later. Some kind of weird security measure.
439 // http://stackoverflow.com/q/12586482/319266
440 // Works: jsfiddle.net/zJzMy/1
441 // Fails: jsfiddle.net/uJTQz
442 // Works again: http://jsfiddle.net/Azr4w/ (diff: the next 3 lines)
443 if ( nextnode.jquery ) {
444 nextnode = nextnode.get( 0 );
446 nextnode.parentNode.insertBefore( s, nextnode );
448 document.getElementsByTagName( 'head' )[0].appendChild( s );
450 if ( s.styleSheet ) {
452 s.styleSheet.cssText = text;
455 // (Safari sometimes borks on non-string values,
456 // play safe by casting to a string, just in case.)
457 s.appendChild( document.createTextNode( String( text ) ) );
463 * Checks whether it is safe to add this css to a stylesheet.
466 * @param {string} cssText
467 * @return {boolean} False if a new one must be created.
469 function canExpandStylesheetWith( cssText ) {
470 // Makes sure that cssText containing `@import`
471 // rules will end up in a new stylesheet (as those only work when
472 // placed at the start of a stylesheet; bug 35562).
473 return cssText.indexOf( '@import' ) === -1;
477 * @param {string} [cssText=cssBuffer] If called without cssText,
478 * the internal buffer will be inserted instead.
479 * @param {Function} [callback]
481 function addEmbeddedCSS( cssText, callback ) {
485 cssCallbacks.add( callback );
488 // Yield once before inserting the <style> tag. There are likely
489 // more calls coming up which we can combine this way.
490 // Appending a stylesheet and waiting for the browser to repaint
491 // is fairly expensive, this reduces it (bug 45810)
493 // Be careful not to extend the buffer with css that needs a new stylesheet
494 if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) {
495 // Linebreak for somewhat distinguishable sections
496 // (the rl-cachekey comment separating each)
497 cssBuffer += '\n' + cssText;
498 // TODO: Use requestAnimationFrame in the future which will
499 // perform even better by not injecting styles while the browser
501 setTimeout( function () {
502 // Can't pass addEmbeddedCSS to setTimeout directly because Firefox
503 // (below version 13) has the non-standard behaviour of passing a
504 // numerical "lateness" value as first argument to this callback
505 // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/
511 // This is a delayed call and we got a buffer still
512 } else if ( cssBuffer ) {
516 // This is a delayed call, but buffer is already cleared by
517 // another delayed call.
521 // By default, always create a new <style>. Appending text
522 // to a <style> tag means the contents have to be re-parsed (bug 45810).
523 // Except, of course, in IE below 9, in there we default to
524 // re-using and appending to a <style> tag due to the
525 // IE stylesheet limit (bug 31676).
526 if ( 'documentMode' in document && document.documentMode <= 9 ) {
528 $style = getMarker().prev();
529 // Verify that the the element before Marker actually is a
530 // <style> tag and one that came from ResourceLoader
531 // (not some other style tag or even a `<meta>` or `<script>`).
532 if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) {
533 // There's already a dynamic <style> tag present and
534 // canExpandStylesheetWith() gave a green light to append more to it.
535 styleEl = $style.get( 0 );
536 if ( styleEl.styleSheet ) {
538 styleEl.styleSheet.cssText += cssText; // IE
540 log( 'addEmbeddedCSS fail\ne.message: ' + e.message, e );
543 styleEl.appendChild( document.createTextNode( String( cssText ) ) );
545 cssCallbacks.fire().empty();
550 $( addStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true );
552 cssCallbacks.fire().empty();
556 * Generates an ISO8601 "basic" string from a UNIX timestamp
559 function formatVersionNumber( timestamp ) {
561 function pad( a, b, c ) {
562 return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
564 d.setTime( timestamp * 1000 );
566 pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T',
567 pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z'
572 * Resolves dependencies and detects circular references.
575 * @param {string} module Name of the top-level module whose dependencies shall be
576 * resolved and sorted.
577 * @param {Array} resolved Returns a topological sort of the given module and its
578 * dependencies, such that later modules depend on earlier modules. The array
579 * contains the module names. If the array contains already some module names,
580 * this function appends its result to the pre-existing array.
581 * @param {Object} [unresolved] Hash used to track the current dependency
582 * chain; used to report loops in the dependency graph.
583 * @throws {Error} If any unregistered module or a dependency loop is encountered
585 function sortDependencies( module, resolved, unresolved ) {
588 if ( registry[module] === undefined ) {
589 throw new Error( 'Unknown dependency: ' + module );
591 // Resolves dynamic loader function and replaces it with its own results
592 if ( $.isFunction( registry[module].dependencies ) ) {
593 registry[module].dependencies = registry[module].dependencies();
594 // Ensures the module's dependencies are always in an array
595 if ( typeof registry[module].dependencies !== 'object' ) {
596 registry[module].dependencies = [registry[module].dependencies];
599 if ( $.inArray( module, resolved ) !== -1 ) {
600 // Module already resolved; nothing to do.
603 // unresolved is optional, supply it if not passed in
607 // Tracks down dependencies
608 deps = registry[module].dependencies;
610 for ( n = 0; n < len; n += 1 ) {
611 if ( $.inArray( deps[n], resolved ) === -1 ) {
612 if ( unresolved[deps[n]] ) {
614 'Circular reference detected: ' + module +
620 unresolved[module] = true;
621 sortDependencies( deps[n], resolved, unresolved );
622 delete unresolved[module];
625 resolved[resolved.length] = module;
629 * Gets a list of module names that a module depends on in their proper dependency
633 * @param {string} module Module name or array of string module names
634 * @return {Array} list of dependencies, including 'module'.
635 * @throws {Error} If circular reference is detected
637 function resolve( module ) {
640 // Allow calling with an array of module names
641 if ( $.isArray( module ) ) {
643 for ( m = 0; m < module.length; m += 1 ) {
644 sortDependencies( module[m], resolved );
649 if ( typeof module === 'string' ) {
651 sortDependencies( module, resolved );
655 throw new Error( 'Invalid module argument: ' + module );
659 * Narrows a list of module names down to those matching a specific
660 * state (see comment on top of this scope for a list of valid states).
661 * One can also filter for 'unregistered', which will return the
662 * modules names that don't have a registry entry.
665 * @param {string|string[]} states Module states to filter by
666 * @param {Array} [modules] List of module names to filter (optional, by default the entire
668 * @return {Array} List of filtered module names
670 function filter( states, modules ) {
671 var list, module, s, m;
673 // Allow states to be given as a string
674 if ( typeof states === 'string' ) {
677 // If called without a list of modules, build and use a list of all modules
679 if ( modules === undefined ) {
681 for ( module in registry ) {
682 modules[modules.length] = module;
685 // Build a list of modules which are in one of the specified states
686 for ( s = 0; s < states.length; s += 1 ) {
687 for ( m = 0; m < modules.length; m += 1 ) {
688 if ( registry[modules[m]] === undefined ) {
689 // Module does not exist
690 if ( states[s] === 'unregistered' ) {
692 list[list.length] = modules[m];
695 // Module exists, check state
696 if ( registry[modules[m]].state === states[s] ) {
698 list[list.length] = modules[m];
707 * Determine whether all dependencies are in state 'ready', which means we may
708 * execute the module or job now.
711 * @param {Array} dependencies Dependencies (module names) to be checked.
712 * @return {boolean} True if all dependencies are in state 'ready', false otherwise
714 function allReady( dependencies ) {
715 return filter( 'ready', dependencies ).length === dependencies.length;
719 * Log a message to window.console, if possible. Useful to force logging of some
720 * errors that are otherwise hard to detect (I.e., this logs also in production mode).
721 * Gets console references in each invocation, so that delayed debugging tools work
722 * fine. No need for optimization here, which would only result in losing logs.
725 * @param {string} msg text for the log entry.
728 function log( msg, e ) {
729 var console = window.console;
730 if ( console && console.log ) {
732 // If we have an exception object, log it through .error() to trigger
733 // proper stacktraces in browsers that support it. There are no (known)
734 // browsers that don't support .error(), that do support .log() and
735 // have useful exception handling through .log().
736 if ( e && console.error ) {
743 * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs
744 * and modules that depend upon this module. if the given module failed, propagate the 'error'
745 * state up the dependency tree; otherwise, execute all jobs/modules that now have all their
746 * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any.
749 * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
751 function handlePending( module ) {
752 var j, job, hasErrors, m, stateChange;
755 if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) {
756 // If the current module failed, mark all dependent modules also as failed.
757 // Iterate until steady-state to propagate the error state upwards in the
761 for ( m in registry ) {
762 if ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) {
763 if ( filter( ['error', 'missing'], registry[m].dependencies ).length > 0 ) {
764 registry[m].state = 'error';
769 } while ( stateChange );
772 // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
773 for ( j = 0; j < jobs.length; j += 1 ) {
774 hasErrors = filter( ['error', 'missing'], jobs[j].dependencies ).length > 0;
775 if ( hasErrors || allReady( jobs[j].dependencies ) ) {
776 // All dependencies satisfied, or some have errors
782 throw new Error( 'Module ' + module + ' failed.');
784 if ( $.isFunction( job.ready ) ) {
789 if ( $.isFunction( job.error ) ) {
791 job.error( e, [module] );
793 // A user-defined operation raised an exception. Swallow to protect
794 // our state machine!
795 log( 'Exception thrown by job.error()', ex );
802 if ( registry[module].state === 'ready' ) {
803 // The current module became 'ready'. Recursively execute all dependent modules that are loaded
804 // and now have all dependencies satisfied.
805 for ( m in registry ) {
806 if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) {
814 * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
815 * depending on whether document-ready has occurred yet and whether we are in async mode.
818 * @param {string} src URL to script, will be used as the src attribute in the script tag
819 * @param {Function} [callback] Callback which will be run when the script is done
821 function addScript( src, callback, async ) {
822 /*jshint evil:true */
826 // Using isReady directly instead of storing it locally from
827 // a $.fn.ready callback (bug 31895).
828 if ( $.isReady || async ) {
829 // Can't use jQuery.getScript because that only uses <script> for cross-domain,
830 // it uses XHR and eval for same-domain scripts, which we don't want because it
831 // messes up line numbers.
832 // The below is based on jQuery ([jquery@1.8.2]/src/ajax/script.js)
834 // IE-safe way of getting the <head>. document.head isn't supported
835 // in old IE, and doesn't work when in the <head>.
836 head = document.getElementsByTagName( 'head' )[0] || document.body;
838 script = document.createElement( 'script' );
841 if ( $.isFunction( callback ) ) {
842 script.onload = script.onreadystatechange = function () {
847 || /loaded|complete/.test( script.readyState )
852 // Handle memory leak in IE
853 script.onload = script.onreadystatechange = null;
856 if ( script.parentNode ) {
857 script.parentNode.removeChild( script );
860 // Dereference the script
868 if ( window.opera ) {
869 // Appending to the <head> blocks rendering completely in Opera,
870 // so append to the <body> after document ready. This means the
871 // scripts only start loading after the document has been rendered,
872 // but so be it. Opera users don't deserve faster web pages if their
873 // browser makes it impossible.
875 document.body.appendChild( script );
878 head.appendChild( script );
881 document.write( mw.html.element( 'script', { 'src': src }, '' ) );
882 if ( $.isFunction( callback ) ) {
883 // Document.write is synchronous, so this is called when it's done
884 // FIXME: that's a lie. doc.write isn't actually synchronous
891 * Executes a loaded module, making it ready to use
894 * @param {string} module Module name to execute
896 function execute( module ) {
897 var key, value, media, i, urls, cssHandle, checkCssHandles,
898 cssHandlesRegistered = false;
900 if ( registry[module] === undefined ) {
901 throw new Error( 'Module has not been registered yet: ' + module );
902 } else if ( registry[module].state === 'registered' ) {
903 throw new Error( 'Module has not been requested from the server yet: ' + module );
904 } else if ( registry[module].state === 'loading' ) {
905 throw new Error( 'Module has not completed loading yet: ' + module );
906 } else if ( registry[module].state === 'ready' ) {
907 throw new Error( 'Module has already been executed: ' + module );
911 * Define loop-function here for efficiency
912 * and to avoid re-using badly scoped variables.
915 function addLink( media, url ) {
916 var el = document.createElement( 'link' );
917 getMarker().before( el ); // IE: Insert in dom before setting href
918 el.rel = 'stylesheet';
919 if ( media && media !== 'all' ) {
925 function runScript() {
926 var script, markModuleReady, nestedAddScript;
928 script = registry[module].script;
929 markModuleReady = function () {
930 registry[module].state = 'ready';
931 handlePending( module );
933 nestedAddScript = function ( arr, callback, async, i ) {
934 // Recursively call addScript() in its own callback
935 // for each element of arr.
936 if ( i >= arr.length ) {
937 // We're at the end of the array
942 addScript( arr[i], function () {
943 nestedAddScript( arr, callback, async, i + 1 );
947 if ( $.isArray( script ) ) {
948 nestedAddScript( script, markModuleReady, registry[module].async, 0 );
949 } else if ( $.isFunction( script ) ) {
950 registry[module].state = 'ready';
952 handlePending( module );
955 // This needs to NOT use mw.log because these errors are common in production mode
956 // and not in debug mode, such as when a symbol that should be global isn't exported
957 log( 'Exception thrown by ' + module + ': ' + e.message, e );
958 registry[module].state = 'error';
959 handlePending( module );
963 // This used to be inside runScript, but since that is now fired asychronously
964 // (after CSS is loaded) we need to set it here right away. It is crucial that
965 // when execute() is called this is set synchronously, otherwise modules will get
966 // executed multiple times as the registry will state that it isn't loading yet.
967 registry[module].state = 'loading';
969 // Add localizations to message system
970 if ( $.isPlainObject( registry[module].messages ) ) {
971 mw.messages.set( registry[module].messages );
974 // Make sure we don't run the scripts until all (potentially asynchronous)
975 // stylesheet insertions have completed.
978 checkCssHandles = function () {
979 // cssHandlesRegistered ensures we don't take off too soon, e.g. when
980 // one of the cssHandles is fired while we're still creating more handles.
981 if ( cssHandlesRegistered && pending === 0 && runScript ) {
983 runScript = undefined; // Revoke
986 cssHandle = function () {
987 var check = checkCssHandles;
993 check = undefined; // Revoke
999 // Process styles (see also mw.loader.implement)
1000 // * back-compat: { <media>: css }
1001 // * back-compat: { <media>: [url, ..] }
1002 // * { "css": [css, ..] }
1003 // * { "url": { <media>: [url, ..] } }
1004 if ( $.isPlainObject( registry[module].style ) ) {
1005 for ( key in registry[module].style ) {
1006 value = registry[module].style[key];
1009 if ( key !== 'url' && key !== 'css' ) {
1010 // Backwards compatibility, key is a media-type
1011 if ( typeof value === 'string' ) {
1012 // back-compat: { <media>: css }
1013 // Ignore 'media' because it isn't supported (nor was it used).
1014 // Strings are pre-wrapped in "@media". The media-type was just ""
1015 // (because it had to be set to something).
1016 // This is one of the reasons why this format is no longer used.
1017 addEmbeddedCSS( value, cssHandle() );
1019 // back-compat: { <media>: [url, ..] }
1025 // Array of css strings in key 'css',
1026 // or back-compat array of urls from media-type
1027 if ( $.isArray( value ) ) {
1028 for ( i = 0; i < value.length; i += 1 ) {
1029 if ( key === 'bc-url' ) {
1030 // back-compat: { <media>: [url, ..] }
1031 addLink( media, value[i] );
1032 } else if ( key === 'css' ) {
1033 // { "css": [css, ..] }
1034 addEmbeddedCSS( value[i], cssHandle() );
1037 // Not an array, but a regular object
1038 // Array of urls inside media-type key
1039 } else if ( typeof value === 'object' ) {
1040 // { "url": { <media>: [url, ..] } }
1041 for ( media in value ) {
1042 urls = value[media];
1043 for ( i = 0; i < urls.length; i += 1 ) {
1044 addLink( media, urls[i] );
1052 cssHandlesRegistered = true;
1057 * Adds a dependencies to the queue with optional callbacks to be run
1058 * when the dependencies are ready or fail
1061 * @param {string|string[]} dependencies Module name or array of string module names
1062 * @param {Function} [ready] Callback to execute when all dependencies are ready
1063 * @param {Function} [error] Callback to execute when any dependency fails
1064 * @param {boolean} [async] If true, load modules asynchronously even if
1065 * document ready has not yet occurred.
1067 function request( dependencies, ready, error, async ) {
1070 // Allow calling by single module name
1071 if ( typeof dependencies === 'string' ) {
1072 dependencies = [dependencies];
1075 // Add ready and error callbacks if they were given
1076 if ( ready !== undefined || error !== undefined ) {
1077 jobs[jobs.length] = {
1078 'dependencies': filter(
1079 ['registered', 'loading', 'loaded'],
1087 // Queue up any dependencies that are registered
1088 dependencies = filter( ['registered'], dependencies );
1089 for ( n = 0; n < dependencies.length; n += 1 ) {
1090 if ( $.inArray( dependencies[n], queue ) === -1 ) {
1091 queue[queue.length] = dependencies[n];
1093 // Mark this module as async in the registry
1094 registry[dependencies[n]].async = true;
1103 function sortQuery(o) {
1104 var sorted = {}, key, a = [];
1106 if ( hasOwn.call( o, key ) ) {
1111 for ( key = 0; key < a.length; key += 1 ) {
1112 sorted[a[key]] = o[a[key]];
1118 * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
1119 * to a query string of the form foo.bar,baz|bar.baz,quux
1122 function buildModulesString( moduleMap ) {
1123 var arr = [], p, prefix;
1124 for ( prefix in moduleMap ) {
1125 p = prefix === '' ? '' : prefix + '.';
1126 arr.push( p + moduleMap[prefix].join( ',' ) );
1128 return arr.join( '|' );
1132 * Asynchronously append a script tag to the end of the body
1133 * that invokes load.php
1135 * @param {Object} moduleMap Module map, see #buildModulesString
1136 * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
1137 * @param {string} sourceLoadScript URL of load.php
1138 * @param {boolean} async If true, use an asynchrounous request even if document ready has not yet occurred
1140 function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) {
1141 var request = $.extend(
1142 { modules: buildModulesString( moduleMap ) },
1145 request = sortQuery( request );
1146 // Asynchronously append a script tag to the end of the body
1147 // Append &* to avoid triggering the IE6 extension check
1148 addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
1151 /* Public Methods */
1153 addStyleTag: addStyleTag,
1156 * Requests dependencies from server, loading and executing when things when ready.
1159 var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
1160 source, group, g, i, modules, maxVersion, sourceLoadScript,
1161 currReqBase, currReqBaseLength, moduleMap, l,
1162 lastDotIndex, prefix, suffix, bytesAdded, async;
1164 // Build a list of request parameters common to all requests.
1166 skin: mw.config.get( 'skin' ),
1167 lang: mw.config.get( 'wgUserLanguage' ),
1168 debug: mw.config.get( 'debug' )
1170 // Split module batch by source and by group.
1172 maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 );
1174 // Appends a list of modules from the queue to the batch
1175 for ( q = 0; q < queue.length; q += 1 ) {
1176 // Only request modules which are registered
1177 if ( registry[queue[q]] !== undefined && registry[queue[q]].state === 'registered' ) {
1178 // Prevent duplicate entries
1179 if ( $.inArray( queue[q], batch ) === -1 ) {
1180 batch[batch.length] = queue[q];
1181 // Mark registered modules as loading
1182 registry[queue[q]].state = 'loading';
1186 // Early exit if there's nothing to load...
1187 if ( !batch.length ) {
1191 // The queue has been processed into the batch, clear up the queue.
1194 // Always order modules alphabetically to help reduce cache
1195 // misses for otherwise identical content.
1198 // Split batch by source and by group.
1199 for ( b = 0; b < batch.length; b += 1 ) {
1200 bSource = registry[batch[b]].source;
1201 bGroup = registry[batch[b]].group;
1202 if ( splits[bSource] === undefined ) {
1203 splits[bSource] = {};
1205 if ( splits[bSource][bGroup] === undefined ) {
1206 splits[bSource][bGroup] = [];
1208 bSourceGroup = splits[bSource][bGroup];
1209 bSourceGroup[bSourceGroup.length] = batch[b];
1212 // Clear the batch - this MUST happen before we append any
1213 // script elements to the body or it's possible that a script
1214 // will be locally cached, instantly load, and work the batch
1215 // again, all before we've cleared it causing each request to
1216 // include modules which are already loaded.
1219 for ( source in splits ) {
1221 sourceLoadScript = sources[source].loadScript;
1223 for ( group in splits[source] ) {
1225 // Cache access to currently selected list of
1226 // modules for this group from this source.
1227 modules = splits[source][group];
1229 // Calculate the highest timestamp
1231 for ( g = 0; g < modules.length; g += 1 ) {
1232 if ( registry[modules[g]].version > maxVersion ) {
1233 maxVersion = registry[modules[g]].version;
1237 currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase );
1238 // For user modules append a user name to the request.
1239 if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
1240 currReqBase.user = mw.config.get( 'wgUserName' );
1242 currReqBaseLength = $.param( currReqBase ).length;
1244 // We may need to split up the request to honor the query string length limit,
1245 // so build it piece by piece.
1246 l = currReqBaseLength + 9; // '&modules='.length == 9
1248 moduleMap = {}; // { prefix: [ suffixes ] }
1250 for ( i = 0; i < modules.length; i += 1 ) {
1251 // Determine how many bytes this module would add to the query string
1252 lastDotIndex = modules[i].lastIndexOf( '.' );
1253 // Note that these substr() calls work even if lastDotIndex == -1
1254 prefix = modules[i].substr( 0, lastDotIndex );
1255 suffix = modules[i].substr( lastDotIndex + 1 );
1256 bytesAdded = moduleMap[prefix] !== undefined
1257 ? suffix.length + 3 // '%2C'.length == 3
1258 : modules[i].length + 3; // '%7C'.length == 3
1260 // If the request would become too long, create a new one,
1261 // but don't create empty requests
1262 if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
1263 // This request would become too long, create a new one
1264 // and fire off the old one
1265 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1268 l = currReqBaseLength + 9;
1270 if ( moduleMap[prefix] === undefined ) {
1271 moduleMap[prefix] = [];
1273 moduleMap[prefix].push( suffix );
1274 if ( !registry[modules[i]].async ) {
1275 // If this module is blocking, make the entire request blocking
1276 // This is slightly suboptimal, but in practice mixing of blocking
1277 // and async modules will only occur in debug mode.
1282 // If there's anything left in moduleMap, request that too
1283 if ( !$.isEmptyObject( moduleMap ) ) {
1284 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1291 * Register a source.
1293 * @param {string} id Short lowercase a-Z string representing a source, only used internally.
1294 * @param {Object} props Object containing only the loadScript property which is a url to
1295 * the load.php location of the source.
1298 addSource: function ( id, props ) {
1300 // Allow multiple additions
1301 if ( typeof id === 'object' ) {
1302 for ( source in id ) {
1303 mw.loader.addSource( source, id[source] );
1308 if ( sources[id] !== undefined ) {
1309 throw new Error( 'source already registered: ' + id );
1312 sources[id] = props;
1318 * Registers a module, letting the system know about it and its
1319 * properties. Startup modules contain calls to this function.
1321 * @param {string} module Module name
1322 * @param {number} version Module version number as a timestamp (falls backs to 0)
1323 * @param {string|Array|Function} dependencies One string or array of strings of module
1324 * names on which this module depends, or a function that returns that array.
1325 * @param {string} [group=null] Group which the module is in
1326 * @param {string} [source='local'] Name of the source
1328 register: function ( module, version, dependencies, group, source ) {
1330 // Allow multiple registration
1331 if ( typeof module === 'object' ) {
1332 for ( m = 0; m < module.length; m += 1 ) {
1333 // module is an array of module names
1334 if ( typeof module[m] === 'string' ) {
1335 mw.loader.register( module[m] );
1336 // module is an array of arrays
1337 } else if ( typeof module[m] === 'object' ) {
1338 mw.loader.register.apply( mw.loader, module[m] );
1344 if ( typeof module !== 'string' ) {
1345 throw new Error( 'module must be a string, not a ' + typeof module );
1347 if ( registry[module] !== undefined ) {
1348 throw new Error( 'module already registered: ' + module );
1350 // List the module as registered
1351 registry[module] = {
1352 version: version !== undefined ? parseInt( version, 10 ) : 0,
1354 group: typeof group === 'string' ? group : null,
1355 source: typeof source === 'string' ? source: 'local',
1358 if ( typeof dependencies === 'string' ) {
1359 // Allow dependencies to be given as a single module name
1360 registry[module].dependencies = [ dependencies ];
1361 } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
1362 // Allow dependencies to be given as an array of module names
1363 // or a function which returns an array
1364 registry[module].dependencies = dependencies;
1369 * Implements a module, giving the system a course of action to take
1370 * upon loading. Results of a request for one or more modules contain
1371 * calls to this function.
1373 * All arguments are required.
1375 * @param {string} module Name of module
1376 * @param {Function|Array} script Function with module code or Array of URLs to
1377 * be used as the src attribute of a new `<script>` tag.
1378 * @param {Object} style Should follow one of the following patterns:
1379 * { "css": [css, ..] }
1380 * { "url": { <media>: [url, ..] } }
1381 * And for backwards compatibility (needs to be supported forever due to caching):
1383 * { <media>: [url, ..] }
1385 * The reason css strings are not concatenated anymore is bug 31676. We now check
1386 * whether it's safe to extend the stylesheet (see #canExpandStylesheetWith).
1388 * @param {Object} msgs List of key/value pairs to be added to {@link mw#messages}.
1390 implement: function ( module, script, style, msgs ) {
1392 if ( typeof module !== 'string' ) {
1393 throw new Error( 'module must be a string, not a ' + typeof module );
1395 if ( !$.isFunction( script ) && !$.isArray( script ) ) {
1396 throw new Error( 'script must be a function or an array, not a ' + typeof script );
1398 if ( !$.isPlainObject( style ) ) {
1399 throw new Error( 'style must be an object, not a ' + typeof style );
1401 if ( !$.isPlainObject( msgs ) ) {
1402 throw new Error( 'msgs must be an object, not a ' + typeof msgs );
1404 // Automatically register module
1405 if ( registry[module] === undefined ) {
1406 mw.loader.register( module );
1408 // Check for duplicate implementation
1409 if ( registry[module] !== undefined && registry[module].script !== undefined ) {
1410 throw new Error( 'module already implemented: ' + module );
1412 // Attach components
1413 registry[module].script = script;
1414 registry[module].style = style;
1415 registry[module].messages = msgs;
1416 // The module may already have been marked as erroneous
1417 if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
1418 registry[module].state = 'loaded';
1419 if ( allReady( registry[module].dependencies ) ) {
1426 * Executes a function as soon as one or more required modules are ready
1428 * @param {string|Array} dependencies Module name or array of modules names the callback
1429 * dependends on to be ready before executing
1430 * @param {Function} [ready] callback to execute when all dependencies are ready
1431 * @param {Function} [error] callback to execute when if dependencies have a errors
1433 using: function ( dependencies, ready, error ) {
1434 var tod = typeof dependencies;
1436 if ( tod !== 'object' && tod !== 'string' ) {
1437 throw new Error( 'dependencies must be a string or an array, not a ' + tod );
1439 // Allow calling with a single dependency as a string
1440 if ( tod === 'string' ) {
1441 dependencies = [ dependencies ];
1443 // Resolve entire dependency map
1444 dependencies = resolve( dependencies );
1445 if ( allReady( dependencies ) ) {
1446 // Run ready immediately
1447 if ( $.isFunction( ready ) ) {
1450 } else if ( filter( ['error', 'missing'], dependencies ).length ) {
1451 // Execute error immediately if any dependencies have errors
1452 if ( $.isFunction( error ) ) {
1453 error( new Error( 'one or more dependencies have state "error" or "missing"' ),
1457 // Not all dependencies are ready: queue up a request
1458 request( dependencies, ready, error );
1463 * Loads an external script or one or more modules for future use
1465 * @param {string|Array} modules Either the name of a module, array of modules,
1466 * or a URL of an external script or style
1467 * @param {string} [type='text/javascript'] mime-type to use if calling with a URL of an
1468 * external script or style; acceptable values are "text/css" and
1469 * "text/javascript"; if no type is provided, text/javascript is assumed.
1470 * @param {boolean} [async] If true, load modules asynchronously
1471 * even if document ready has not yet occurred. If false, block before
1472 * document ready and load async after. If not set, true will be
1473 * assumed if loading a URL, and false will be assumed otherwise.
1475 load: function ( modules, type, async ) {
1476 var filtered, m, module, l;
1479 if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
1480 throw new Error( 'modules must be a string or an array, not a ' + typeof modules );
1482 // Allow calling with an external url or single dependency as a string
1483 if ( typeof modules === 'string' ) {
1484 // Support adding arbitrary external scripts
1485 if ( /^(https?:)?\/\//.test( modules ) ) {
1486 if ( async === undefined ) {
1487 // Assume async for bug 34542
1490 if ( type === 'text/css' ) {
1491 // IE7-8 throws security warnings when inserting a <link> tag
1492 // with a protocol-relative URL set though attributes (instead of
1493 // properties) - when on HTTPS. See also bug #.
1494 l = document.createElement( 'link' );
1495 l.rel = 'stylesheet';
1497 $( 'head' ).append( l );
1500 if ( type === 'text/javascript' || type === undefined ) {
1501 addScript( modules, null, async );
1505 throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
1507 // Called with single module
1508 modules = [ modules ];
1511 // Filter out undefined modules, otherwise resolve() will throw
1512 // an exception for trying to load an undefined module.
1513 // Undefined modules are acceptable here in load(), because load() takes
1514 // an array of unrelated modules, whereas the modules passed to
1515 // using() are related and must all be loaded.
1516 for ( filtered = [], m = 0; m < modules.length; m += 1 ) {
1517 module = registry[modules[m]];
1518 if ( module !== undefined ) {
1519 if ( $.inArray( module.state, ['error', 'missing'] ) === -1 ) {
1520 filtered[filtered.length] = modules[m];
1525 if ( filtered.length === 0 ) {
1528 // Resolve entire dependency map
1529 filtered = resolve( filtered );
1530 // If all modules are ready, nothing to be done
1531 if ( allReady( filtered ) ) {
1534 // If any modules have errors: also quit.
1535 if ( filter( ['error', 'missing'], filtered ).length ) {
1538 // Since some modules are not yet ready, queue up a request.
1539 request( filtered, undefined, undefined, async );
1543 * Changes the state of a module
1545 * @param {string|Object} module module name or object of module name/state pairs
1546 * @param {string} state state name
1548 state: function ( module, state ) {
1551 if ( typeof module === 'object' ) {
1552 for ( m in module ) {
1553 mw.loader.state( m, module[m] );
1557 if ( registry[module] === undefined ) {
1558 mw.loader.register( module );
1560 if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1
1561 && registry[module].state !== state ) {
1562 // Make sure pending modules depending on this one get executed if their
1563 // dependencies are now fulfilled!
1564 registry[module].state = state;
1565 handlePending( module );
1567 registry[module].state = state;
1572 * Gets the version of a module
1574 * @param {string} module name of module to get version for
1576 getVersion: function ( module ) {
1577 if ( registry[module] !== undefined && registry[module].version !== undefined ) {
1578 return formatVersionNumber( registry[module].version );
1584 * @deprecated since 1.18 use mw.loader.getVersion() instead
1586 version: function () {
1587 return mw.loader.getVersion.apply( mw.loader, arguments );
1591 * Gets the state of a module
1593 * @param {string} module name of module to get state for
1595 getState: function ( module ) {
1596 if ( registry[module] !== undefined && registry[module].state !== undefined ) {
1597 return registry[module].state;
1603 * Get names of all registered modules.
1607 getModuleNames: function () {
1608 return $.map( registry, function ( i, key ) {
1614 * For backwards-compatibility with Squid-cached pages. Loads mw.user
1617 mw.loader.load( 'mediawiki.user' );
1623 * HTML construction helper functions
1627 html: ( function () {
1628 function escapeCallback( s ) {
1645 * Escape a string for HTML. Converts special characters to HTML entities.
1646 * @param {string} s The string to escape
1648 escape: function ( s ) {
1649 return s.replace( /['"<>&]/g, escapeCallback );
1653 * Wrapper object for raw HTML passed to mw.html.element().
1654 * @class mw.html.Raw
1656 Raw: function ( value ) {
1661 * Wrapper object for CDATA element contents passed to mw.html.element()
1662 * @class mw.html.Cdata
1664 Cdata: function ( value ) {
1669 * Create an HTML element string, with safe escaping.
1671 * @param {string} name The tag name.
1672 * @param {Object} attrs An object with members mapping element names to values
1673 * @param {Mixed} contents The contents of the element. May be either:
1674 * - string: The string is escaped.
1675 * - null or undefined: The short closing form is used, e.g. <br/>.
1676 * - this.Raw: The value attribute is included without escaping.
1677 * - this.Cdata: The value attribute is included, and an exception is
1678 * thrown if it contains an illegal ETAGO delimiter.
1679 * See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2
1683 * return h.element( 'div', {},
1684 * new h.Raw( h.element( 'img', {src: '<'} ) ) );
1685 * Returns <div><img src="<"/></div>
1687 element: function ( name, attrs, contents ) {
1688 var v, attrName, s = '<' + name;
1690 for ( attrName in attrs ) {
1691 v = attrs[attrName];
1692 // Convert name=true, to name=name
1696 } else if ( v === false ) {
1699 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
1701 if ( contents === undefined || contents === null ) {
1708 switch ( typeof contents ) {
1711 s += this.escape( contents );
1715 // Convert to string
1716 s += String( contents );
1719 if ( contents instanceof this.Raw ) {
1720 // Raw HTML inclusion
1721 s += contents.value;
1722 } else if ( contents instanceof this.Cdata ) {
1724 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
1725 throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
1727 s += contents.value;
1729 throw new Error( 'mw.html.element: Invalid type of contents' );
1732 s += '</' + name + '>';
1738 // Skeleton user object. mediawiki.user.js extends this
1745 * Registry and firing of events.
1747 * MediaWiki has various interface components that are extended, enhanced
1748 * or manipulated in some other way by extensions, gadgets and even
1751 * This framework helps streamlining the timing of when these other
1752 * code paths fire their plugins (instead of using document-ready,
1753 * which can and should be limited to firing only once).
1755 * Features like navigating to other wiki pages, previewing an edit
1756 * and editing itself – without a refresh – can then retrigger these
1757 * hooks accordingly to ensure everything still works as expected.
1761 * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
1762 * mw.hook( 'wikipage.content' ).fire( $content );
1764 * Handlers can be added and fired for arbitrary event names at any time. The same
1765 * event can be fired multiple times. The last run of an event is memorized
1766 * (similar to `$(document).ready` and `$.Deferred().done`).
1767 * This means if an event is fired, and a handler added afterwards, the added
1768 * function will be fired right away with the last given event data.
1770 * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
1771 * Thus allowing flexible use and optimal maintainability and authority control.
1772 * You can pass around the `add` and/or `fire` method to another piece of code
1773 * without it having to know the event name (or `mw.hook` for that matter).
1775 * var h = mw.hook( 'bar.ready' );
1776 * new mw.Foo( .. ).fetch( { callback: h.fire } );
1780 hook: ( function () {
1786 * @param {string} name Name of hook.
1789 return function ( name ) {
1790 var list = lists[name] || ( lists[name] = $.Callbacks( 'memory' ) );
1794 * Register a hook handler
1795 * @param {Function...} handler Function to bind.
1801 * Unregister a hook handler
1802 * @param {Function...} handler Function to unbind.
1805 remove: list.remove,
1809 * @param {Mixed...} data
1813 return list.fireWith( null, slice.call( arguments ) );
1822 // Alias $j to jQuery for backwards compatibility
1825 // Attach to window and globally alias
1826 window.mw = window.mediaWiki = mw;
1828 // Auto-register from pre-loaded startup scripts
1829 if ( jQuery.isFunction( window.startUp ) ) {
1831 window.startUp = undefined;