2 * Base library for MediaWiki.
5 * @alternateClassName mediaWiki
9 var mw = ( function ( $, undefined ) {
14 var hasOwn = Object.prototype.hasOwnProperty,
15 slice = Array.prototype.slice;
18 * Log a message to window.console, if possible. Useful to force logging of some
19 * errors that are otherwise hard to detect (I.e., this logs also in production mode).
20 * Gets console references in each invocation, so that delayed debugging tools work
21 * fine. No need for optimization here, which would only result in losing logs.
24 * @param {string} msg text for the log entry.
27 function log( msg, e ) {
28 var console = window.console;
29 if ( console && console.log ) {
31 // If we have an exception object, log it through .error() to trigger
32 // proper stacktraces in browsers that support it. There are no (known)
33 // browsers that don't support .error(), that do support .log() and
34 // have useful exception handling through .log().
35 if ( e && console.error ) {
36 console.error( String( e ), e );
41 /* Object constructors */
44 * Creates an object that can be read from or written to from prototype functions
45 * that allow both single and multiple variables at once.
49 * var addies, wanted, results;
51 * // Create your address book
52 * addies = new mw.Map();
54 * // This data could be coming from an external source (eg. API/AJAX)
56 * 'John Doe' : '10 Wall Street, New York, USA',
57 * 'Jane Jackson' : '21 Oxford St, London, UK',
58 * 'Dominique van Halen' : 'Kalverstraat 7, Amsterdam, NL'
61 * wanted = ['Dominique van Halen', 'George Johnson', 'Jane Jackson'];
63 * // You can detect missing keys first
64 * if ( !addies.exists( wanted ) ) {
65 * // One or more are missing (in this case: "George Johnson")
66 * mw.log( 'One or more names were not found in your address book' );
69 * // Or just let it give you what it can
70 * results = addies.get( wanted, 'Middle of Nowhere, Alaska, US' );
71 * mw.log( results['Jane Jackson'] ); // "21 Oxford St, London, UK"
72 * mw.log( results['George Johnson'] ); // "Middle of Nowhere, Alaska, US"
77 * @param {boolean} [global=false] Whether to store the values in the global window
78 * object or a exclusively in the object property 'values'.
80 function Map( global ) {
81 this.values = global === true ? window : {};
87 * Get the value of one or multiple a keys.
89 * If called with no arguments, all values will be returned.
91 * @param {string|Array} selection String key or array of keys to get values for.
92 * @param {Mixed} [fallback] Value to use in case key(s) do not exist.
93 * @return mixed If selection was a string returns the value or null,
94 * If selection was an array, returns an object of key/values (value is null if not found),
95 * If selection was not passed or invalid, will return the 'values' object member (be careful as
96 * objects are always passed by reference in JavaScript!).
97 * @return {string|Object|null} Values as a string or object, null if invalid/inexistant.
99 get: function ( selection, fallback ) {
101 // If we only do this in the `return` block, it'll fail for the
102 // call to get() from the mutli-selection block.
103 fallback = arguments.length > 1 ? fallback : null;
105 if ( $.isArray( selection ) ) {
106 selection = slice.call( selection );
108 for ( i = 0; i < selection.length; i++ ) {
109 results[selection[i]] = this.get( selection[i], fallback );
114 if ( typeof selection === 'string' ) {
115 if ( !hasOwn.call( this.values, selection ) ) {
118 return this.values[selection];
121 if ( selection === undefined ) {
125 // invalid selection key
130 * Sets one or multiple key/value pairs.
132 * @param {string|Object} selection String key to set value for, or object mapping keys to values.
133 * @param {Mixed} [value] Value to set (optional, only in use when key is a string)
134 * @return {Boolean} This returns true on success, false on failure.
136 set: function ( selection, value ) {
139 if ( $.isPlainObject( selection ) ) {
140 for ( s in selection ) {
141 this.values[s] = selection[s];
145 if ( typeof selection === 'string' && arguments.length > 1 ) {
146 this.values[selection] = value;
153 * Checks if one or multiple keys exist.
155 * @param {Mixed} selection String key or array of keys to check
156 * @return {boolean} Existence of key(s)
158 exists: function ( selection ) {
161 if ( $.isArray( selection ) ) {
162 for ( s = 0; s < selection.length; s++ ) {
163 if ( typeof selection[s] !== 'string' || !hasOwn.call( this.values, selection[s] ) ) {
169 return typeof selection === 'string' && hasOwn.call( this.values, selection );
174 * Object constructor for messages.
176 * Similar to the Message class in MediaWiki PHP.
178 * Format defaults to 'text'.
183 * @param {mw.Map} map Message storage
184 * @param {string} key
185 * @param {Array} [parameters]
187 function Message( map, key, parameters ) {
188 this.format = 'text';
191 this.parameters = parameters === undefined ? [] : slice.call( parameters );
195 Message.prototype = {
197 * Simple message parser, does $N replacement and nothing else.
199 * This may be overridden to provide a more complex message parser.
201 * The primary override is in mediawiki.jqueryMsg.
203 * This function will not be called for nonexistent messages.
205 parser: function () {
206 var parameters = this.parameters;
207 return this.map.get( this.key ).replace( /\$(\d+)/g, function ( str, match ) {
208 var index = parseInt( match, 10 ) - 1;
209 return parameters[index] !== undefined ? parameters[index] : '$' + match;
214 * Appends (does not replace) parameters for replacement to the .parameters property.
216 * @param {Array} parameters
219 params: function ( parameters ) {
221 for ( i = 0; i < parameters.length; i += 1 ) {
222 this.parameters.push( parameters[i] );
228 * Converts message object to it's string form based on the state of format.
230 * @return {string} Message as a string in the current form or `<key>` if key does not exist.
232 toString: function () {
235 if ( !this.exists() ) {
236 // Use <key> as text if key does not exist
237 if ( this.format === 'escaped' || this.format === 'parse' ) {
238 // format 'escaped' and 'parse' need to have the brackets and key html escaped
239 return mw.html.escape( '<' + this.key + '>' );
241 return '<' + this.key + '>';
244 if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) {
245 text = this.parser();
248 if ( this.format === 'escaped' ) {
249 text = this.parser();
250 text = mw.html.escape( text );
257 * Changes format to 'parse' and converts message to string
259 * If jqueryMsg is loaded, this parses the message text from wikitext
260 * (where supported) to HTML
262 * Otherwise, it is equivalent to plain.
264 * @return {string} String form of parsed message
267 this.format = 'parse';
268 return this.toString();
272 * Changes format to 'plain' and converts message to string
274 * This substitutes parameters, but otherwise does not change the
277 * @return {string} String form of plain message
280 this.format = 'plain';
281 return this.toString();
285 * Changes format to 'text' and converts message to string
287 * If jqueryMsg is loaded, {{-transformation is done where supported
288 * (such as {{plural:}}, {{gender:}}, {{int:}}).
290 * Otherwise, it is equivalent to plain.
293 this.format = 'text';
294 return this.toString();
298 * Changes the format to 'escaped' and converts message to string
300 * This is equivalent to using the 'text' format (see text method), then
301 * HTML-escaping the output.
303 * @return {string} String form of html escaped message
305 escaped: function () {
306 this.format = 'escaped';
307 return this.toString();
311 * Checks if message exists
316 exists: function () {
317 return this.map.exists( this.key );
325 * Dummy placeholder for {@link mw.log}
329 var log = function () {};
330 log.warn = function () {};
331 log.deprecate = function ( obj, key, val ) {
337 // Make the Map constructor publicly available.
340 // Make the Message constructor publicly available.
344 * Map of configuration values
346 * Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config)
349 * If `$wgLegacyJavaScriptGlobals` is true, this Map will put its values in the
350 * global window object.
352 * @property {mw.Map} config
354 // Dummy placeholder. Re-assigned in ResourceLoaderStartupModule with an instance of `mw.Map`.
358 * Empty object that plugins can be installed in.
364 * Access container for deprecated functionality that can be moved from
365 * from their legacy location and attached to this object (e.g. a global
366 * function that is deprecated and as stop-gap can be exposed through here).
368 * This was reserved for future use but never ended up being used.
370 * @deprecated since 1.22: Let deprecated identifiers keep their original name
371 * and use mw.log#deprecate to create an access container for tracking.
377 * Localization system
385 * Get a message object.
387 * Similar to wfMessage() in MediaWiki PHP.
389 * @param {string} key Key of message to get
390 * @param {Mixed...} parameters Parameters for the $N replacements in messages.
391 * @return {mw.Message}
393 message: function ( key ) {
394 // Variadic arguments
395 var parameters = slice.call( arguments, 1 );
396 return new Message( mw.messages, key, parameters );
400 * Get a message string using 'text' format.
402 * Similar to wfMsg() in MediaWiki PHP.
405 * @param {string} key Key of message to get
406 * @param {Mixed...} parameters Parameters for the $N replacements in messages.
410 return mw.message.apply( mw.message, arguments ).toString();
414 * Client-side module loader which integrates with the MediaWiki ResourceLoader
418 loader: ( function () {
420 /* Private Members */
423 * Mapping of registered modules
425 * The jquery module is pre-registered, because it must have already
426 * been provided for this object to have been built, and in debug mode
427 * jquery would have been provided through a unique loader request,
428 * making it impossible to hold back registration of jquery until after
431 * For exact details on support for script, style and messages, look at
432 * mw.loader.implement.
437 * 'version': ############## (unix timestamp),
438 * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
439 * 'group': 'somegroup', (or) null,
440 * 'source': 'local', 'someforeignwiki', (or) null
441 * 'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing'
444 * 'messages': { 'key': 'value' },
453 // Mapping of sources, keyed by source-id, values are objects.
457 // 'loadScript': 'http://foo.bar/w/load.php'
462 // List of modules which will be loaded as when ready
464 // List of modules to be loaded
466 // List of callback functions waiting for modules to be ready to be called
468 // Selector cache for the marker element. Use getMarker() to get/use the marker!
470 // Buffer for addEmbeddedCSS.
472 // Callbacks for addEmbeddedCSS.
473 cssCallbacks = $.Callbacks();
475 /* Private methods */
477 function getMarker() {
483 $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
484 if ( $marker.length ) {
487 mw.log( 'getMarker> No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically.' );
488 $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' );
494 * Create a new style tag and add it to the DOM.
497 * @param {string} text CSS text
498 * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag should be
499 * inserted before. Otherwise it will be appended to `<head>`.
500 * @return {HTMLElement} Reference to the created `<style>` element.
502 function newStyleTag( text, nextnode ) {
503 var s = document.createElement( 'style' );
504 // Insert into document before setting cssText (bug 33305)
506 // Must be inserted with native insertBefore, not $.fn.before.
507 // When using jQuery to insert it, like $nextnode.before( s ),
508 // then IE6 will throw "Access is denied" when trying to append
509 // to .cssText later. Some kind of weird security measure.
510 // http://stackoverflow.com/q/12586482/319266
511 // Works: jsfiddle.net/zJzMy/1
512 // Fails: jsfiddle.net/uJTQz
513 // Works again: http://jsfiddle.net/Azr4w/ (diff: the next 3 lines)
514 if ( nextnode.jquery ) {
515 nextnode = nextnode.get( 0 );
517 nextnode.parentNode.insertBefore( s, nextnode );
519 document.getElementsByTagName( 'head' )[0].appendChild( s );
521 if ( s.styleSheet ) {
523 s.styleSheet.cssText = text;
526 // (Safari sometimes borks on non-string values,
527 // play safe by casting to a string, just in case.)
528 s.appendChild( document.createTextNode( String( text ) ) );
534 * Checks whether it is safe to add this css to a stylesheet.
537 * @param {string} cssText
538 * @return {boolean} False if a new one must be created.
540 function canExpandStylesheetWith( cssText ) {
541 // Makes sure that cssText containing `@import`
542 // rules will end up in a new stylesheet (as those only work when
543 // placed at the start of a stylesheet; bug 35562).
544 return cssText.indexOf( '@import' ) === -1;
548 * Add a bit of CSS text to the current browser page.
550 * The CSS will be appended to an existing ResourceLoader-created `<style>` tag
551 * or create a new one based on whether the given `cssText` is safe for extension.
553 * @param {string} [cssText=cssBuffer] If called without cssText,
554 * the internal buffer will be inserted instead.
555 * @param {Function} [callback]
557 function addEmbeddedCSS( cssText, callback ) {
561 cssCallbacks.add( callback );
564 // Yield once before inserting the <style> tag. There are likely
565 // more calls coming up which we can combine this way.
566 // Appending a stylesheet and waiting for the browser to repaint
567 // is fairly expensive, this reduces it (bug 45810)
569 // Be careful not to extend the buffer with css that needs a new stylesheet
570 if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) {
571 // Linebreak for somewhat distinguishable sections
572 // (the rl-cachekey comment separating each)
573 cssBuffer += '\n' + cssText;
574 // TODO: Use requestAnimationFrame in the future which will
575 // perform even better by not injecting styles while the browser
577 setTimeout( function () {
578 // Can't pass addEmbeddedCSS to setTimeout directly because Firefox
579 // (below version 13) has the non-standard behaviour of passing a
580 // numerical "lateness" value as first argument to this callback
581 // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/
587 // This is a delayed call and we got a buffer still
588 } else if ( cssBuffer ) {
592 // This is a delayed call, but buffer is already cleared by
593 // another delayed call.
597 // By default, always create a new <style>. Appending text
598 // to a <style> tag means the contents have to be re-parsed (bug 45810).
599 // Except, of course, in IE below 9, in there we default to
600 // re-using and appending to a <style> tag due to the
601 // IE stylesheet limit (bug 31676).
602 if ( 'documentMode' in document && document.documentMode <= 9 ) {
604 $style = getMarker().prev();
605 // Verify that the the element before Marker actually is a
606 // <style> tag and one that came from ResourceLoader
607 // (not some other style tag or even a `<meta>` or `<script>`).
608 if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) {
609 // There's already a dynamic <style> tag present and
610 // canExpandStylesheetWith() gave a green light to append more to it.
611 styleEl = $style.get( 0 );
612 if ( styleEl.styleSheet ) {
614 styleEl.styleSheet.cssText += cssText; // IE
616 log( 'addEmbeddedCSS fail', e );
619 styleEl.appendChild( document.createTextNode( String( cssText ) ) );
621 cssCallbacks.fire().empty();
626 $( newStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true );
628 cssCallbacks.fire().empty();
632 * Generates an ISO8601 "basic" string from a UNIX timestamp
635 function formatVersionNumber( timestamp ) {
637 function pad( a, b, c ) {
638 return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
640 d.setTime( timestamp * 1000 );
642 pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T',
643 pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z'
648 * Resolves dependencies and detects circular references.
651 * @param {string} module Name of the top-level module whose dependencies shall be
652 * resolved and sorted.
653 * @param {Array} resolved Returns a topological sort of the given module and its
654 * dependencies, such that later modules depend on earlier modules. The array
655 * contains the module names. If the array contains already some module names,
656 * this function appends its result to the pre-existing array.
657 * @param {Object} [unresolved] Hash used to track the current dependency
658 * chain; used to report loops in the dependency graph.
659 * @throws {Error} If any unregistered module or a dependency loop is encountered
661 function sortDependencies( module, resolved, unresolved ) {
664 if ( registry[module] === undefined ) {
665 throw new Error( 'Unknown dependency: ' + module );
667 // Resolves dynamic loader function and replaces it with its own results
668 if ( $.isFunction( registry[module].dependencies ) ) {
669 registry[module].dependencies = registry[module].dependencies();
670 // Ensures the module's dependencies are always in an array
671 if ( typeof registry[module].dependencies !== 'object' ) {
672 registry[module].dependencies = [registry[module].dependencies];
675 if ( $.inArray( module, resolved ) !== -1 ) {
676 // Module already resolved; nothing to do.
679 // unresolved is optional, supply it if not passed in
683 // Tracks down dependencies
684 deps = registry[module].dependencies;
686 for ( n = 0; n < len; n += 1 ) {
687 if ( $.inArray( deps[n], resolved ) === -1 ) {
688 if ( unresolved[deps[n]] ) {
690 'Circular reference detected: ' + module +
696 unresolved[module] = true;
697 sortDependencies( deps[n], resolved, unresolved );
698 delete unresolved[module];
701 resolved[resolved.length] = module;
705 * Gets a list of module names that a module depends on in their proper dependency
709 * @param {string} module Module name or array of string module names
710 * @return {Array} list of dependencies, including 'module'.
711 * @throws {Error} If circular reference is detected
713 function resolve( module ) {
716 // Allow calling with an array of module names
717 if ( $.isArray( module ) ) {
719 for ( m = 0; m < module.length; m += 1 ) {
720 sortDependencies( module[m], resolved );
725 if ( typeof module === 'string' ) {
727 sortDependencies( module, resolved );
731 throw new Error( 'Invalid module argument: ' + module );
735 * Narrows a list of module names down to those matching a specific
736 * state (see comment on top of this scope for a list of valid states).
737 * One can also filter for 'unregistered', which will return the
738 * modules names that don't have a registry entry.
741 * @param {string|string[]} states Module states to filter by
742 * @param {Array} [modules] List of module names to filter (optional, by default the entire
744 * @return {Array} List of filtered module names
746 function filter( states, modules ) {
747 var list, module, s, m;
749 // Allow states to be given as a string
750 if ( typeof states === 'string' ) {
753 // If called without a list of modules, build and use a list of all modules
755 if ( modules === undefined ) {
757 for ( module in registry ) {
758 modules[modules.length] = module;
761 // Build a list of modules which are in one of the specified states
762 for ( s = 0; s < states.length; s += 1 ) {
763 for ( m = 0; m < modules.length; m += 1 ) {
764 if ( registry[modules[m]] === undefined ) {
765 // Module does not exist
766 if ( states[s] === 'unregistered' ) {
768 list[list.length] = modules[m];
771 // Module exists, check state
772 if ( registry[modules[m]].state === states[s] ) {
774 list[list.length] = modules[m];
783 * Determine whether all dependencies are in state 'ready', which means we may
784 * execute the module or job now.
787 * @param {Array} dependencies Dependencies (module names) to be checked.
788 * @return {boolean} True if all dependencies are in state 'ready', false otherwise
790 function allReady( dependencies ) {
791 return filter( 'ready', dependencies ).length === dependencies.length;
795 * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs
796 * and modules that depend upon this module. if the given module failed, propagate the 'error'
797 * state up the dependency tree; otherwise, execute all jobs/modules that now have all their
798 * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any.
801 * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
803 function handlePending( module ) {
804 var j, job, hasErrors, m, stateChange;
807 if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) {
808 // If the current module failed, mark all dependent modules also as failed.
809 // Iterate until steady-state to propagate the error state upwards in the
813 for ( m in registry ) {
814 if ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) {
815 if ( filter( ['error', 'missing'], registry[m].dependencies ).length > 0 ) {
816 registry[m].state = 'error';
821 } while ( stateChange );
824 // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
825 for ( j = 0; j < jobs.length; j += 1 ) {
826 hasErrors = filter( ['error', 'missing'], jobs[j].dependencies ).length > 0;
827 if ( hasErrors || allReady( jobs[j].dependencies ) ) {
828 // All dependencies satisfied, or some have errors
834 if ( $.isFunction( job.error ) ) {
835 job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [module] );
838 if ( $.isFunction( job.ready ) ) {
843 // A user-defined callback raised an exception.
844 // Swallow it to protect our state machine!
845 log( 'Exception thrown by job.error', e );
850 if ( registry[module].state === 'ready' ) {
851 // The current module became 'ready'. Recursively execute all dependent modules that are loaded
852 // and now have all dependencies satisfied.
853 for ( m in registry ) {
854 if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) {
862 * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
863 * depending on whether document-ready has occurred yet and whether we are in async mode.
866 * @param {string} src URL to script, will be used as the src attribute in the script tag
867 * @param {Function} [callback] Callback which will be run when the script is done
869 function addScript( src, callback, async ) {
870 /*jshint evil:true */
871 var script, head, done;
873 // Using isReady directly instead of storing it locally from
874 // a $.fn.ready callback (bug 31895).
875 if ( $.isReady || async ) {
876 // Can't use jQuery.getScript because that only uses <script> for cross-domain,
877 // it uses XHR and eval for same-domain scripts, which we don't want because it
878 // messes up line numbers.
879 // The below is based on jQuery ([jquery@1.8.2]/src/ajax/script.js)
881 // IE-safe way of getting the <head>. document.head isn't supported
882 // in old IE, and doesn't work when in the <head>.
884 head = document.getElementsByTagName( 'head' )[0] || document.body;
886 script = document.createElement( 'script' );
889 if ( $.isFunction( callback ) ) {
890 script.onload = script.onreadystatechange = function () {
895 || /loaded|complete/.test( script.readyState )
900 // Handle memory leak in IE
901 script.onload = script.onreadystatechange = null;
903 // Detach the element from the document
904 if ( script.parentNode ) {
905 script.parentNode.removeChild( script );
908 // Dereference the element from javascript
916 if ( window.opera ) {
917 // Appending to the <head> blocks rendering completely in Opera,
918 // so append to the <body> after document ready. This means the
919 // scripts only start loading after the document has been rendered,
920 // but so be it. Opera users don't deserve faster web pages if their
921 // browser makes it impossible.
923 document.body.appendChild( script );
926 head.appendChild( script );
929 document.write( mw.html.element( 'script', { 'src': src }, '' ) );
930 if ( $.isFunction( callback ) ) {
931 // Document.write is synchronous, so this is called when it's done
932 // FIXME: that's a lie. doc.write isn't actually synchronous
939 * Executes a loaded module, making it ready to use
942 * @param {string} module Module name to execute
944 function execute( module ) {
945 var key, value, media, i, urls, cssHandle, checkCssHandles,
946 cssHandlesRegistered = false;
948 if ( registry[module] === undefined ) {
949 throw new Error( 'Module has not been registered yet: ' + module );
950 } else if ( registry[module].state === 'registered' ) {
951 throw new Error( 'Module has not been requested from the server yet: ' + module );
952 } else if ( registry[module].state === 'loading' ) {
953 throw new Error( 'Module has not completed loading yet: ' + module );
954 } else if ( registry[module].state === 'ready' ) {
955 throw new Error( 'Module has already been executed: ' + module );
959 * Define loop-function here for efficiency
960 * and to avoid re-using badly scoped variables.
963 function addLink( media, url ) {
964 var el = document.createElement( 'link' );
965 getMarker().before( el ); // IE: Insert in dom before setting href
966 el.rel = 'stylesheet';
967 if ( media && media !== 'all' ) {
973 function runScript() {
974 var script, markModuleReady, nestedAddScript;
976 script = registry[module].script;
977 markModuleReady = function () {
978 registry[module].state = 'ready';
979 handlePending( module );
981 nestedAddScript = function ( arr, callback, async, i ) {
982 // Recursively call addScript() in its own callback
983 // for each element of arr.
984 if ( i >= arr.length ) {
985 // We're at the end of the array
990 addScript( arr[i], function () {
991 nestedAddScript( arr, callback, async, i + 1 );
995 if ( $.isArray( script ) ) {
996 nestedAddScript( script, markModuleReady, registry[module].async, 0 );
997 } else if ( $.isFunction( script ) ) {
998 registry[module].state = 'ready';
1000 handlePending( module );
1003 // This needs to NOT use mw.log because these errors are common in production mode
1004 // and not in debug mode, such as when a symbol that should be global isn't exported
1005 log( 'Exception thrown by ' + module, e );
1006 registry[module].state = 'error';
1007 handlePending( module );
1011 // This used to be inside runScript, but since that is now fired asychronously
1012 // (after CSS is loaded) we need to set it here right away. It is crucial that
1013 // when execute() is called this is set synchronously, otherwise modules will get
1014 // executed multiple times as the registry will state that it isn't loading yet.
1015 registry[module].state = 'loading';
1017 // Add localizations to message system
1018 if ( $.isPlainObject( registry[module].messages ) ) {
1019 mw.messages.set( registry[module].messages );
1022 if ( $.isReady || registry[module].async ) {
1023 // Make sure we don't run the scripts until all (potentially asynchronous)
1024 // stylesheet insertions have completed.
1027 checkCssHandles = function () {
1028 // cssHandlesRegistered ensures we don't take off too soon, e.g. when
1029 // one of the cssHandles is fired while we're still creating more handles.
1030 if ( cssHandlesRegistered && pending === 0 && runScript ) {
1032 runScript = undefined; // Revoke
1035 cssHandle = function () {
1036 var check = checkCssHandles;
1038 return function () {
1042 check = undefined; // Revoke
1048 // We are in blocking mode, and so we can't afford to wait for CSS
1049 cssHandle = function () {};
1051 checkCssHandles = runScript;
1054 // Process styles (see also mw.loader.implement)
1055 // * back-compat: { <media>: css }
1056 // * back-compat: { <media>: [url, ..] }
1057 // * { "css": [css, ..] }
1058 // * { "url": { <media>: [url, ..] } }
1059 if ( $.isPlainObject( registry[module].style ) ) {
1060 for ( key in registry[module].style ) {
1061 value = registry[module].style[key];
1064 if ( key !== 'url' && key !== 'css' ) {
1065 // Backwards compatibility, key is a media-type
1066 if ( typeof value === 'string' ) {
1067 // back-compat: { <media>: css }
1068 // Ignore 'media' because it isn't supported (nor was it used).
1069 // Strings are pre-wrapped in "@media". The media-type was just ""
1070 // (because it had to be set to something).
1071 // This is one of the reasons why this format is no longer used.
1072 addEmbeddedCSS( value, cssHandle() );
1074 // back-compat: { <media>: [url, ..] }
1080 // Array of css strings in key 'css',
1081 // or back-compat array of urls from media-type
1082 if ( $.isArray( value ) ) {
1083 for ( i = 0; i < value.length; i += 1 ) {
1084 if ( key === 'bc-url' ) {
1085 // back-compat: { <media>: [url, ..] }
1086 addLink( media, value[i] );
1087 } else if ( key === 'css' ) {
1088 // { "css": [css, ..] }
1089 addEmbeddedCSS( value[i], cssHandle() );
1092 // Not an array, but a regular object
1093 // Array of urls inside media-type key
1094 } else if ( typeof value === 'object' ) {
1095 // { "url": { <media>: [url, ..] } }
1096 for ( media in value ) {
1097 urls = value[media];
1098 for ( i = 0; i < urls.length; i += 1 ) {
1099 addLink( media, urls[i] );
1107 cssHandlesRegistered = true;
1112 * Adds a dependencies to the queue with optional callbacks to be run
1113 * when the dependencies are ready or fail
1116 * @param {string|string[]} dependencies Module name or array of string module names
1117 * @param {Function} [ready] Callback to execute when all dependencies are ready
1118 * @param {Function} [error] Callback to execute when any dependency fails
1119 * @param {boolean} [async] If true, load modules asynchronously even if
1120 * document ready has not yet occurred.
1122 function request( dependencies, ready, error, async ) {
1125 // Allow calling by single module name
1126 if ( typeof dependencies === 'string' ) {
1127 dependencies = [dependencies];
1130 // Add ready and error callbacks if they were given
1131 if ( ready !== undefined || error !== undefined ) {
1132 jobs[jobs.length] = {
1133 'dependencies': filter(
1134 ['registered', 'loading', 'loaded'],
1142 // Queue up any dependencies that are registered
1143 dependencies = filter( ['registered'], dependencies );
1144 for ( n = 0; n < dependencies.length; n += 1 ) {
1145 if ( $.inArray( dependencies[n], queue ) === -1 ) {
1146 queue[queue.length] = dependencies[n];
1148 // Mark this module as async in the registry
1149 registry[dependencies[n]].async = true;
1158 function sortQuery(o) {
1159 var sorted = {}, key, a = [];
1161 if ( hasOwn.call( o, key ) ) {
1166 for ( key = 0; key < a.length; key += 1 ) {
1167 sorted[a[key]] = o[a[key]];
1173 * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
1174 * to a query string of the form foo.bar,baz|bar.baz,quux
1177 function buildModulesString( moduleMap ) {
1178 var arr = [], p, prefix;
1179 for ( prefix in moduleMap ) {
1180 p = prefix === '' ? '' : prefix + '.';
1181 arr.push( p + moduleMap[prefix].join( ',' ) );
1183 return arr.join( '|' );
1187 * Asynchronously append a script tag to the end of the body
1188 * that invokes load.php
1190 * @param {Object} moduleMap Module map, see #buildModulesString
1191 * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
1192 * @param {string} sourceLoadScript URL of load.php
1193 * @param {boolean} async If true, use an asynchronous request even if document ready has not yet occurred
1195 function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) {
1196 var request = $.extend(
1197 { modules: buildModulesString( moduleMap ) },
1200 request = sortQuery( request );
1201 // Asynchronously append a script tag to the end of the body
1202 // Append &* to avoid triggering the IE6 extension check
1203 addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
1206 /* Public Methods */
1209 * The module registry is exposed as an aid for debugging and inspecting page
1210 * state; it is not a public interface for modifying the registry.
1216 moduleRegistry: registry,
1219 * @inheritdoc #newStyleTag
1222 addStyleTag: newStyleTag,
1225 * Batch-request queued dependencies from the server.
1228 var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
1229 source, group, g, i, modules, maxVersion, sourceLoadScript,
1230 currReqBase, currReqBaseLength, moduleMap, l,
1231 lastDotIndex, prefix, suffix, bytesAdded, async;
1233 // Build a list of request parameters common to all requests.
1235 skin: mw.config.get( 'skin' ),
1236 lang: mw.config.get( 'wgUserLanguage' ),
1237 debug: mw.config.get( 'debug' )
1239 // Split module batch by source and by group.
1241 maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 );
1243 // Appends a list of modules from the queue to the batch
1244 for ( q = 0; q < queue.length; q += 1 ) {
1245 // Only request modules which are registered
1246 if ( registry[queue[q]] !== undefined && registry[queue[q]].state === 'registered' ) {
1247 // Prevent duplicate entries
1248 if ( $.inArray( queue[q], batch ) === -1 ) {
1249 batch[batch.length] = queue[q];
1250 // Mark registered modules as loading
1251 registry[queue[q]].state = 'loading';
1255 // Early exit if there's nothing to load...
1256 if ( !batch.length ) {
1260 // The queue has been processed into the batch, clear up the queue.
1263 // Always order modules alphabetically to help reduce cache
1264 // misses for otherwise identical content.
1267 // Split batch by source and by group.
1268 for ( b = 0; b < batch.length; b += 1 ) {
1269 bSource = registry[batch[b]].source;
1270 bGroup = registry[batch[b]].group;
1271 if ( splits[bSource] === undefined ) {
1272 splits[bSource] = {};
1274 if ( splits[bSource][bGroup] === undefined ) {
1275 splits[bSource][bGroup] = [];
1277 bSourceGroup = splits[bSource][bGroup];
1278 bSourceGroup[bSourceGroup.length] = batch[b];
1281 // Clear the batch - this MUST happen before we append any
1282 // script elements to the body or it's possible that a script
1283 // will be locally cached, instantly load, and work the batch
1284 // again, all before we've cleared it causing each request to
1285 // include modules which are already loaded.
1288 for ( source in splits ) {
1290 sourceLoadScript = sources[source].loadScript;
1292 for ( group in splits[source] ) {
1294 // Cache access to currently selected list of
1295 // modules for this group from this source.
1296 modules = splits[source][group];
1298 // Calculate the highest timestamp
1300 for ( g = 0; g < modules.length; g += 1 ) {
1301 if ( registry[modules[g]].version > maxVersion ) {
1302 maxVersion = registry[modules[g]].version;
1306 currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase );
1307 // For user modules append a user name to the request.
1308 if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
1309 currReqBase.user = mw.config.get( 'wgUserName' );
1311 currReqBaseLength = $.param( currReqBase ).length;
1313 // We may need to split up the request to honor the query string length limit,
1314 // so build it piece by piece.
1315 l = currReqBaseLength + 9; // '&modules='.length == 9
1317 moduleMap = {}; // { prefix: [ suffixes ] }
1319 for ( i = 0; i < modules.length; i += 1 ) {
1320 // Determine how many bytes this module would add to the query string
1321 lastDotIndex = modules[i].lastIndexOf( '.' );
1322 // Note that these substr() calls work even if lastDotIndex == -1
1323 prefix = modules[i].substr( 0, lastDotIndex );
1324 suffix = modules[i].substr( lastDotIndex + 1 );
1325 bytesAdded = moduleMap[prefix] !== undefined
1326 ? suffix.length + 3 // '%2C'.length == 3
1327 : modules[i].length + 3; // '%7C'.length == 3
1329 // If the request would become too long, create a new one,
1330 // but don't create empty requests
1331 if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
1332 // This request would become too long, create a new one
1333 // and fire off the old one
1334 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1337 l = currReqBaseLength + 9;
1339 if ( moduleMap[prefix] === undefined ) {
1340 moduleMap[prefix] = [];
1342 moduleMap[prefix].push( suffix );
1343 if ( !registry[modules[i]].async ) {
1344 // If this module is blocking, make the entire request blocking
1345 // This is slightly suboptimal, but in practice mixing of blocking
1346 // and async modules will only occur in debug mode.
1351 // If there's anything left in moduleMap, request that too
1352 if ( !$.isEmptyObject( moduleMap ) ) {
1353 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1360 * Register a source.
1362 * @param {string} id Short lowercase a-Z string representing a source, only used internally.
1363 * @param {Object} props Object containing only the loadScript property which is a url to
1364 * the load.php location of the source.
1367 addSource: function ( id, props ) {
1369 // Allow multiple additions
1370 if ( typeof id === 'object' ) {
1371 for ( source in id ) {
1372 mw.loader.addSource( source, id[source] );
1377 if ( sources[id] !== undefined ) {
1378 throw new Error( 'source already registered: ' + id );
1381 sources[id] = props;
1387 * Register a module, letting the system know about it and its
1388 * properties. Startup modules contain calls to this function.
1390 * @param {string} module Module name
1391 * @param {number} version Module version number as a timestamp (falls backs to 0)
1392 * @param {string|Array|Function} dependencies One string or array of strings of module
1393 * names on which this module depends, or a function that returns that array.
1394 * @param {string} [group=null] Group which the module is in
1395 * @param {string} [source='local'] Name of the source
1397 register: function ( module, version, dependencies, group, source ) {
1399 // Allow multiple registration
1400 if ( typeof module === 'object' ) {
1401 for ( m = 0; m < module.length; m += 1 ) {
1402 // module is an array of module names
1403 if ( typeof module[m] === 'string' ) {
1404 mw.loader.register( module[m] );
1405 // module is an array of arrays
1406 } else if ( typeof module[m] === 'object' ) {
1407 mw.loader.register.apply( mw.loader, module[m] );
1413 if ( typeof module !== 'string' ) {
1414 throw new Error( 'module must be a string, not a ' + typeof module );
1416 if ( registry[module] !== undefined ) {
1417 throw new Error( 'module already registered: ' + module );
1419 // List the module as registered
1420 registry[module] = {
1421 version: version !== undefined ? parseInt( version, 10 ) : 0,
1423 group: typeof group === 'string' ? group : null,
1424 source: typeof source === 'string' ? source: 'local',
1427 if ( typeof dependencies === 'string' ) {
1428 // Allow dependencies to be given as a single module name
1429 registry[module].dependencies = [ dependencies ];
1430 } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
1431 // Allow dependencies to be given as an array of module names
1432 // or a function which returns an array
1433 registry[module].dependencies = dependencies;
1438 * Implement a module given the components that make up the module.
1440 * When #load or #using requests one or more modules, the server
1441 * response contain calls to this function.
1443 * All arguments are required.
1445 * @param {string} module Name of module
1446 * @param {Function|Array} script Function with module code or Array of URLs to
1447 * be used as the src attribute of a new `<script>` tag.
1448 * @param {Object} style Should follow one of the following patterns:
1449 * { "css": [css, ..] }
1450 * { "url": { <media>: [url, ..] } }
1451 * And for backwards compatibility (needs to be supported forever due to caching):
1453 * { <media>: [url, ..] }
1455 * The reason css strings are not concatenated anymore is bug 31676. We now check
1456 * whether it's safe to extend the stylesheet (see #canExpandStylesheetWith).
1458 * @param {Object} msgs List of key/value pairs to be added to {@link mw#messages}.
1460 implement: function ( module, script, style, msgs ) {
1462 if ( typeof module !== 'string' ) {
1463 throw new Error( 'module must be a string, not a ' + typeof module );
1465 if ( !$.isFunction( script ) && !$.isArray( script ) ) {
1466 throw new Error( 'script must be a function or an array, not a ' + typeof script );
1468 if ( !$.isPlainObject( style ) ) {
1469 throw new Error( 'style must be an object, not a ' + typeof style );
1471 if ( !$.isPlainObject( msgs ) ) {
1472 throw new Error( 'msgs must be an object, not a ' + typeof msgs );
1474 // Automatically register module
1475 if ( registry[module] === undefined ) {
1476 mw.loader.register( module );
1478 // Check for duplicate implementation
1479 if ( registry[module] !== undefined && registry[module].script !== undefined ) {
1480 throw new Error( 'module already implemented: ' + module );
1482 // Attach components
1483 registry[module].script = script;
1484 registry[module].style = style;
1485 registry[module].messages = msgs;
1486 // The module may already have been marked as erroneous
1487 if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
1488 registry[module].state = 'loaded';
1489 if ( allReady( registry[module].dependencies ) ) {
1496 * Execute a function as soon as one or more required modules are ready.
1498 * @param {string|Array} dependencies Module name or array of modules names the callback
1499 * dependends on to be ready before executing
1500 * @param {Function} [ready] callback to execute when all dependencies are ready
1501 * @param {Function} [error] callback to execute when if dependencies have a errors
1503 using: function ( dependencies, ready, error ) {
1504 var tod = typeof dependencies;
1506 if ( tod !== 'object' && tod !== 'string' ) {
1507 throw new Error( 'dependencies must be a string or an array, not a ' + tod );
1509 // Allow calling with a single dependency as a string
1510 if ( tod === 'string' ) {
1511 dependencies = [ dependencies ];
1513 // Resolve entire dependency map
1514 dependencies = resolve( dependencies );
1515 if ( allReady( dependencies ) ) {
1516 // Run ready immediately
1517 if ( $.isFunction( ready ) ) {
1520 } else if ( filter( ['error', 'missing'], dependencies ).length ) {
1521 // Execute error immediately if any dependencies have errors
1522 if ( $.isFunction( error ) ) {
1523 error( new Error( 'one or more dependencies have state "error" or "missing"' ),
1527 // Not all dependencies are ready: queue up a request
1528 request( dependencies, ready, error );
1533 * Load an external script or one or more modules.
1535 * @param {string|Array} modules Either the name of a module, array of modules,
1536 * or a URL of an external script or style
1537 * @param {string} [type='text/javascript'] mime-type to use if calling with a URL of an
1538 * external script or style; acceptable values are "text/css" and
1539 * "text/javascript"; if no type is provided, text/javascript is assumed.
1540 * @param {boolean} [async] If true, load modules asynchronously
1541 * even if document ready has not yet occurred. If false, block before
1542 * document ready and load async after. If not set, true will be
1543 * assumed if loading a URL, and false will be assumed otherwise.
1545 load: function ( modules, type, async ) {
1546 var filtered, m, module, l;
1549 if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
1550 throw new Error( 'modules must be a string or an array, not a ' + typeof modules );
1552 // Allow calling with an external url or single dependency as a string
1553 if ( typeof modules === 'string' ) {
1554 // Support adding arbitrary external scripts
1555 if ( /^(https?:)?\/\//.test( modules ) ) {
1556 if ( async === undefined ) {
1557 // Assume async for bug 34542
1560 if ( type === 'text/css' ) {
1561 // IE7-8 throws security warnings when inserting a <link> tag
1562 // with a protocol-relative URL set though attributes (instead of
1563 // properties) - when on HTTPS. See also bug #.
1564 l = document.createElement( 'link' );
1565 l.rel = 'stylesheet';
1567 $( 'head' ).append( l );
1570 if ( type === 'text/javascript' || type === undefined ) {
1571 addScript( modules, null, async );
1575 throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
1577 // Called with single module
1578 modules = [ modules ];
1581 // Filter out undefined modules, otherwise resolve() will throw
1582 // an exception for trying to load an undefined module.
1583 // Undefined modules are acceptable here in load(), because load() takes
1584 // an array of unrelated modules, whereas the modules passed to
1585 // using() are related and must all be loaded.
1586 for ( filtered = [], m = 0; m < modules.length; m += 1 ) {
1587 module = registry[modules[m]];
1588 if ( module !== undefined ) {
1589 if ( $.inArray( module.state, ['error', 'missing'] ) === -1 ) {
1590 filtered[filtered.length] = modules[m];
1595 if ( filtered.length === 0 ) {
1598 // Resolve entire dependency map
1599 filtered = resolve( filtered );
1600 // If all modules are ready, nothing to be done
1601 if ( allReady( filtered ) ) {
1604 // If any modules have errors: also quit.
1605 if ( filter( ['error', 'missing'], filtered ).length ) {
1608 // Since some modules are not yet ready, queue up a request.
1609 request( filtered, undefined, undefined, async );
1613 * Change the state of one or more modules.
1615 * @param {string|Object} module module name or object of module name/state pairs
1616 * @param {string} state state name
1618 state: function ( module, state ) {
1621 if ( typeof module === 'object' ) {
1622 for ( m in module ) {
1623 mw.loader.state( m, module[m] );
1627 if ( registry[module] === undefined ) {
1628 mw.loader.register( module );
1630 if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1
1631 && registry[module].state !== state ) {
1632 // Make sure pending modules depending on this one get executed if their
1633 // dependencies are now fulfilled!
1634 registry[module].state = state;
1635 handlePending( module );
1637 registry[module].state = state;
1642 * Get the version of a module.
1644 * @param {string} module Name of module to get version for
1646 getVersion: function ( module ) {
1647 if ( registry[module] !== undefined && registry[module].version !== undefined ) {
1648 return formatVersionNumber( registry[module].version );
1654 * @inheritdoc #getVersion
1655 * @deprecated since 1.18 use #getVersion instead
1657 version: function () {
1658 return mw.loader.getVersion.apply( mw.loader, arguments );
1662 * Get the state of a module.
1664 * @param {string} module name of module to get state for
1666 getState: function ( module ) {
1667 if ( registry[module] !== undefined && registry[module].state !== undefined ) {
1668 return registry[module].state;
1674 * Get names of all registered modules.
1678 getModuleNames: function () {
1679 return $.map( registry, function ( i, key ) {
1685 * Load the `mediawiki.user` module.
1687 * For backwards-compatibility with cached pages from before 2013 where:
1689 * - the `mediawiki.user` module didn't exist yet
1690 * - `mw.user` was still part of mediawiki.js
1691 * - `mw.loader.go` still existed and called after `mw.loader.load()`
1694 mw.loader.load( 'mediawiki.user' );
1698 * @inheritdoc mw.inspect#runReports
1701 inspect: function () {
1702 var args = slice.call( arguments );
1703 mw.loader.using( 'mediawiki.inspect', function () {
1704 mw.inspect.runReports.apply( mw.inspect, args );
1712 * HTML construction helper functions
1719 * output = Html.element( 'div', {}, new Html.Raw(
1720 * Html.element( 'img', { src: '<' } )
1722 * mw.log( output ); // <div><img src="<"/></div>
1727 html: ( function () {
1728 function escapeCallback( s ) {
1745 * Escape a string for HTML. Converts special characters to HTML entities.
1746 * @param {string} s The string to escape
1748 escape: function ( s ) {
1749 return s.replace( /['"<>&]/g, escapeCallback );
1753 * Create an HTML element string, with safe escaping.
1755 * @param {string} name The tag name.
1756 * @param {Object} attrs An object with members mapping element names to values
1757 * @param {Mixed} contents The contents of the element. May be either:
1758 * - string: The string is escaped.
1759 * - null or undefined: The short closing form is used, e.g. <br/>.
1760 * - this.Raw: The value attribute is included without escaping.
1761 * - this.Cdata: The value attribute is included, and an exception is
1762 * thrown if it contains an illegal ETAGO delimiter.
1763 * See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2
1765 element: function ( name, attrs, contents ) {
1766 var v, attrName, s = '<' + name;
1768 for ( attrName in attrs ) {
1769 v = attrs[attrName];
1770 // Convert name=true, to name=name
1774 } else if ( v === false ) {
1777 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
1779 if ( contents === undefined || contents === null ) {
1786 switch ( typeof contents ) {
1789 s += this.escape( contents );
1793 // Convert to string
1794 s += String( contents );
1797 if ( contents instanceof this.Raw ) {
1798 // Raw HTML inclusion
1799 s += contents.value;
1800 } else if ( contents instanceof this.Cdata ) {
1802 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
1803 throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
1805 s += contents.value;
1807 throw new Error( 'mw.html.element: Invalid type of contents' );
1810 s += '</' + name + '>';
1815 * Wrapper object for raw HTML passed to mw.html.element().
1816 * @class mw.html.Raw
1818 Raw: function ( value ) {
1823 * Wrapper object for CDATA element contents passed to mw.html.element()
1824 * @class mw.html.Cdata
1826 Cdata: function ( value ) {
1832 // Skeleton user object. mediawiki.user.js extends this
1839 * Registry and firing of events.
1841 * MediaWiki has various interface components that are extended, enhanced
1842 * or manipulated in some other way by extensions, gadgets and even
1845 * This framework helps streamlining the timing of when these other
1846 * code paths fire their plugins (instead of using document-ready,
1847 * which can and should be limited to firing only once).
1849 * Features like navigating to other wiki pages, previewing an edit
1850 * and editing itself – without a refresh – can then retrigger these
1851 * hooks accordingly to ensure everything still works as expected.
1855 * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
1856 * mw.hook( 'wikipage.content' ).fire( $content );
1858 * Handlers can be added and fired for arbitrary event names at any time. The same
1859 * event can be fired multiple times. The last run of an event is memorized
1860 * (similar to `$(document).ready` and `$.Deferred().done`).
1861 * This means if an event is fired, and a handler added afterwards, the added
1862 * function will be fired right away with the last given event data.
1864 * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
1865 * Thus allowing flexible use and optimal maintainability and authority control.
1866 * You can pass around the `add` and/or `fire` method to another piece of code
1867 * without it having to know the event name (or `mw.hook` for that matter).
1869 * var h = mw.hook( 'bar.ready' );
1870 * new mw.Foo( .. ).fetch( { callback: h.fire } );
1872 * Note: Events are documented with an underscore instead of a dot in the event
1873 * name due to jsduck not supporting dots in that position.
1877 hook: ( function () {
1881 * Create an instance of mw.hook.
1885 * @param {string} name Name of hook.
1888 return function ( name ) {
1889 var list = lists[name] || ( lists[name] = $.Callbacks( 'memory' ) );
1893 * Register a hook handler
1894 * @param {Function...} handler Function to bind.
1900 * Unregister a hook handler
1901 * @param {Function...} handler Function to unbind.
1904 remove: list.remove,
1908 * @param {Mixed...} data
1912 return list.fireWith( null, slice.call( arguments ) );
1921 // Alias $j to jQuery for backwards compatibility
1924 // Attach to window and globally alias
1925 window.mw = window.mediaWiki = mw;
1927 // Auto-register from pre-loaded startup scripts
1928 if ( jQuery.isFunction( window.startUp ) ) {
1930 window.startUp = undefined;