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 * var addies, wanted, results;
23 * // Create your address book
24 * addies = new mw.Map();
26 * // This data could be coming from an external source (eg. API/AJAX)
28 * 'John Doe' : '10 Wall Street, New York, USA',
29 * 'Jane Jackson' : '21 Oxford St, London, UK',
30 * 'Dominique van Halen' : 'Kalverstraat 7, Amsterdam, NL'
33 * wanted = ['Dominique van Halen', 'George Johnson', 'Jane Jackson'];
35 * // You can detect missing keys first
36 * if ( !addies.exists( wanted ) ) {
37 * // One or more are missing (in this case: "George Johnson")
38 * mw.log( 'One or more names were not found in your address book' );
41 * // Or just let it give you what it can
42 * results = addies.get( wanted, 'Middle of Nowhere, Alaska, US' );
43 * mw.log( results['Jane Jackson'] ); // "21 Oxford St, London, UK"
44 * mw.log( results['George Johnson'] ); // "Middle of Nowhere, Alaska, US"
49 * @param {boolean} [global=false] Whether to store the values in the global window
50 * object or a exclusively in the object property 'values'.
52 function Map( global ) {
53 this.values = global === true ? window : {};
59 * Get the value of one or multiple a keys.
61 * If called with no arguments, all values will be returned.
63 * @param {string|Array} selection String key or array of keys to get values for.
64 * @param {Mixed} [fallback] Value to use in case key(s) do not exist.
65 * @return mixed If selection was a string returns the value or null,
66 * If selection was an array, returns an object of key/values (value is null if not found),
67 * If selection was not passed or invalid, will return the 'values' object member (be careful as
68 * objects are always passed by reference in JavaScript!).
69 * @return {string|Object|null} Values as a string or object, null if invalid/inexistant.
71 get: function ( selection, fallback ) {
73 // If we only do this in the `return` block, it'll fail for the
74 // call to get() from the mutli-selection block.
75 fallback = arguments.length > 1 ? fallback : null;
77 if ( $.isArray( selection ) ) {
78 selection = slice.call( selection );
80 for ( i = 0; i < selection.length; i++ ) {
81 results[selection[i]] = this.get( selection[i], fallback );
86 if ( typeof selection === 'string' ) {
87 if ( !hasOwn.call( this.values, selection ) ) {
90 return this.values[selection];
93 if ( selection === undefined ) {
97 // invalid selection key
102 * Sets one or multiple key/value pairs.
104 * @param {string|Object} selection String key to set value for, or object mapping keys to values.
105 * @param {Mixed} [value] Value to set (optional, only in use when key is a string)
106 * @return {Boolean} This returns true on success, false on failure.
108 set: function ( selection, value ) {
111 if ( $.isPlainObject( selection ) ) {
112 for ( s in selection ) {
113 this.values[s] = selection[s];
117 if ( typeof selection === 'string' && arguments.length > 1 ) {
118 this.values[selection] = value;
125 * Checks if one or multiple keys exist.
127 * @param {Mixed} selection String key or array of keys to check
128 * @return {boolean} Existence of key(s)
130 exists: function ( selection ) {
133 if ( $.isArray( selection ) ) {
134 for ( s = 0; s < selection.length; s++ ) {
135 if ( typeof selection[s] !== 'string' || !hasOwn.call( this.values, selection[s] ) ) {
141 return typeof selection === 'string' && hasOwn.call( this.values, selection );
146 * Object constructor for messages,
147 * similar to the Message class in MediaWiki PHP.
151 * @param {mw.Map} map Message storage
152 * @param {string} key
153 * @param {Array} [parameters]
155 function Message( map, key, parameters ) {
156 this.format = 'text';
159 this.parameters = parameters === undefined ? [] : slice.call( parameters );
163 Message.prototype = {
165 * Simple message parser, does $N replacement and nothing else.
167 * This may be overridden to provide a more complex message parser.
169 * The primary override is in mediawiki.jqueryMsg.
171 * This function will not be called for nonexistent messages.
173 parser: function () {
174 var parameters = this.parameters;
175 return this.map.get( this.key ).replace( /\$(\d+)/g, function ( str, match ) {
176 var index = parseInt( match, 10 ) - 1;
177 return parameters[index] !== undefined ? parameters[index] : '$' + match;
182 * Appends (does not replace) parameters for replacement to the .parameters property.
184 * @param {Array} parameters
187 params: function ( parameters ) {
189 for ( i = 0; i < parameters.length; i += 1 ) {
190 this.parameters.push( parameters[i] );
196 * Converts message object to it's string form based on the state of format.
198 * @return {string} Message as a string in the current form or `<key>` if key does not exist.
200 toString: function () {
203 if ( !this.exists() ) {
204 // Use <key> as text if key does not exist
205 if ( this.format === 'escaped' || this.format === 'parse' ) {
206 // format 'escaped' and 'parse' need to have the brackets and key html escaped
207 return mw.html.escape( '<' + this.key + '>' );
209 return '<' + this.key + '>';
212 if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) {
213 text = this.parser();
216 if ( this.format === 'escaped' ) {
217 text = this.parser();
218 text = mw.html.escape( text );
225 * Changes format to 'parse' and converts message to string
227 * If jqueryMsg is loaded, this parses the message text from wikitext
228 * (where supported) to HTML
230 * Otherwise, it is equivalent to plain.
232 * @return {string} String form of parsed message
235 this.format = 'parse';
236 return this.toString();
240 * Changes format to 'plain' and converts message to string
242 * This substitutes parameters, but otherwise does not change the
245 * @return {string} String form of plain message
248 this.format = 'plain';
249 return this.toString();
253 * Changes format to 'text' and converts message to string
255 * If jqueryMsg is loaded, {{-transformation is done where supported
256 * (such as {{plural:}}, {{gender:}}, {{int:}}).
258 * Otherwise, it is equivalent to plain.
261 this.format = 'text';
262 return this.toString();
266 * Changes the format to 'escaped' and converts message to string
268 * This is equivalent to using the 'text' format (see text method), then
269 * HTML-escaping the output.
271 * @return {string} String form of html escaped message
273 escaped: function () {
274 this.format = 'escaped';
275 return this.toString();
279 * Checks if message exists
284 exists: function () {
285 return this.map.exists( this.key );
290 * Base library for MediaWiki.
293 * @alternateClassName mediaWiki
300 * Dummy placeholder for {@link mw.log}
304 var log = function () {};
305 log.warn = function () {};
306 log.deprecate = function ( obj, key, val ) {
312 // Make the Map constructor publicly available.
315 // Make the Message constructor publicly available.
319 * Map of configuration values
321 * Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config)
324 * If `$wgLegacyJavaScriptGlobals` is true, this Map will put its values in the
325 * global window object.
327 * @property {mw.Map} config
329 // Dummy placeholder. Re-assigned in ResourceLoaderStartupModule with an instance of `mw.Map`.
333 * Empty object that plugins can be installed in.
339 * Access container for deprecated functionality that can be moved from
340 * from their legacy location and attached to this object (e.g. a global
341 * function that is deprecated and as stop-gap can be exposed through here).
343 * This was reserved for future use but never ended up being used.
345 * @deprecated since 1.22: Let deprecated identifiers keep their original name
346 * and use mw.log#deprecate to create an access container for tracking.
352 * Localization system
360 * Gets a message object, similar to wfMessage().
362 * @param {string} key Key of message to get
363 * @param {Mixed...} parameters Parameters for the $N replacements in messages.
364 * @return {mw.Message}
366 message: function ( key ) {
367 // Variadic arguments
368 var parameters = slice.call( arguments, 1 );
369 return new Message( mw.messages, key, parameters );
373 * Gets a message string, similar to wfMessage()
375 * @see mw.Message#toString
376 * @param {string} key Key of message to get
377 * @param {Mixed...} parameters Parameters for the $N replacements in messages.
380 msg: function ( /* key, parameters... */ ) {
381 return mw.message.apply( mw.message, arguments ).toString();
385 * Client-side module loader which integrates with the MediaWiki ResourceLoader
389 loader: ( function () {
391 /* Private Members */
394 * Mapping of registered modules
396 * The jquery module is pre-registered, because it must have already
397 * been provided for this object to have been built, and in debug mode
398 * jquery would have been provided through a unique loader request,
399 * making it impossible to hold back registration of jquery until after
402 * For exact details on support for script, style and messages, look at
403 * mw.loader.implement.
408 * 'version': ############## (unix timestamp),
409 * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
410 * 'group': 'somegroup', (or) null,
411 * 'source': 'local', 'someforeignwiki', (or) null
412 * 'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing'
415 * 'messages': { 'key': 'value' },
424 // Mapping of sources, keyed by source-id, values are objects.
428 // 'loadScript': 'http://foo.bar/w/load.php'
433 // List of modules which will be loaded as when ready
435 // List of modules to be loaded
437 // List of callback functions waiting for modules to be ready to be called
439 // Selector cache for the marker element. Use getMarker() to get/use the marker!
441 // Buffer for addEmbeddedCSS.
443 // Callbacks for addEmbeddedCSS.
444 cssCallbacks = $.Callbacks();
446 /* Private methods */
448 function getMarker() {
454 $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
455 if ( $marker.length ) {
458 mw.log( 'getMarker> No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically.' );
459 $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' );
465 * Create a new style tag and add it to the DOM.
468 * @param {string} text CSS text
469 * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag should be
470 * inserted before. Otherwise it will be appended to `<head>`.
471 * @return {HTMLElement} Reference to the created `<style>` element.
473 function newStyleTag( text, nextnode ) {
474 var s = document.createElement( 'style' );
475 // Insert into document before setting cssText (bug 33305)
477 // Must be inserted with native insertBefore, not $.fn.before.
478 // When using jQuery to insert it, like $nextnode.before( s ),
479 // then IE6 will throw "Access is denied" when trying to append
480 // to .cssText later. Some kind of weird security measure.
481 // http://stackoverflow.com/q/12586482/319266
482 // Works: jsfiddle.net/zJzMy/1
483 // Fails: jsfiddle.net/uJTQz
484 // Works again: http://jsfiddle.net/Azr4w/ (diff: the next 3 lines)
485 if ( nextnode.jquery ) {
486 nextnode = nextnode.get( 0 );
488 nextnode.parentNode.insertBefore( s, nextnode );
490 document.getElementsByTagName( 'head' )[0].appendChild( s );
492 if ( s.styleSheet ) {
494 s.styleSheet.cssText = text;
497 // (Safari sometimes borks on non-string values,
498 // play safe by casting to a string, just in case.)
499 s.appendChild( document.createTextNode( String( text ) ) );
505 * Checks whether it is safe to add this css to a stylesheet.
508 * @param {string} cssText
509 * @return {boolean} False if a new one must be created.
511 function canExpandStylesheetWith( cssText ) {
512 // Makes sure that cssText containing `@import`
513 // rules will end up in a new stylesheet (as those only work when
514 // placed at the start of a stylesheet; bug 35562).
515 return cssText.indexOf( '@import' ) === -1;
519 * Add a bit of CSS text to the current browser page.
521 * The CSS will be appended to an existing ResourceLoader-created `<style>` tag
522 * or create a new one based on whether the given `cssText` is safe for extension.
524 * @param {string} [cssText=cssBuffer] If called without cssText,
525 * the internal buffer will be inserted instead.
526 * @param {Function} [callback]
528 function addEmbeddedCSS( cssText, callback ) {
532 cssCallbacks.add( callback );
535 // Yield once before inserting the <style> tag. There are likely
536 // more calls coming up which we can combine this way.
537 // Appending a stylesheet and waiting for the browser to repaint
538 // is fairly expensive, this reduces it (bug 45810)
540 // Be careful not to extend the buffer with css that needs a new stylesheet
541 if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) {
542 // Linebreak for somewhat distinguishable sections
543 // (the rl-cachekey comment separating each)
544 cssBuffer += '\n' + cssText;
545 // TODO: Use requestAnimationFrame in the future which will
546 // perform even better by not injecting styles while the browser
548 setTimeout( function () {
549 // Can't pass addEmbeddedCSS to setTimeout directly because Firefox
550 // (below version 13) has the non-standard behaviour of passing a
551 // numerical "lateness" value as first argument to this callback
552 // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/
558 // This is a delayed call and we got a buffer still
559 } else if ( cssBuffer ) {
563 // This is a delayed call, but buffer is already cleared by
564 // another delayed call.
568 // By default, always create a new <style>. Appending text
569 // to a <style> tag means the contents have to be re-parsed (bug 45810).
570 // Except, of course, in IE below 9, in there we default to
571 // re-using and appending to a <style> tag due to the
572 // IE stylesheet limit (bug 31676).
573 if ( 'documentMode' in document && document.documentMode <= 9 ) {
575 $style = getMarker().prev();
576 // Verify that the the element before Marker actually is a
577 // <style> tag and one that came from ResourceLoader
578 // (not some other style tag or even a `<meta>` or `<script>`).
579 if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) {
580 // There's already a dynamic <style> tag present and
581 // canExpandStylesheetWith() gave a green light to append more to it.
582 styleEl = $style.get( 0 );
583 if ( styleEl.styleSheet ) {
585 styleEl.styleSheet.cssText += cssText; // IE
587 log( 'addEmbeddedCSS fail\ne.message: ' + e.message, e );
590 styleEl.appendChild( document.createTextNode( String( cssText ) ) );
592 cssCallbacks.fire().empty();
597 $( newStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true );
599 cssCallbacks.fire().empty();
603 * Generates an ISO8601 "basic" string from a UNIX timestamp
606 function formatVersionNumber( timestamp ) {
608 function pad( a, b, c ) {
609 return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
611 d.setTime( timestamp * 1000 );
613 pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T',
614 pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z'
619 * Resolves dependencies and detects circular references.
622 * @param {string} module Name of the top-level module whose dependencies shall be
623 * resolved and sorted.
624 * @param {Array} resolved Returns a topological sort of the given module and its
625 * dependencies, such that later modules depend on earlier modules. The array
626 * contains the module names. If the array contains already some module names,
627 * this function appends its result to the pre-existing array.
628 * @param {Object} [unresolved] Hash used to track the current dependency
629 * chain; used to report loops in the dependency graph.
630 * @throws {Error} If any unregistered module or a dependency loop is encountered
632 function sortDependencies( module, resolved, unresolved ) {
635 if ( registry[module] === undefined ) {
636 throw new Error( 'Unknown dependency: ' + module );
638 // Resolves dynamic loader function and replaces it with its own results
639 if ( $.isFunction( registry[module].dependencies ) ) {
640 registry[module].dependencies = registry[module].dependencies();
641 // Ensures the module's dependencies are always in an array
642 if ( typeof registry[module].dependencies !== 'object' ) {
643 registry[module].dependencies = [registry[module].dependencies];
646 if ( $.inArray( module, resolved ) !== -1 ) {
647 // Module already resolved; nothing to do.
650 // unresolved is optional, supply it if not passed in
654 // Tracks down dependencies
655 deps = registry[module].dependencies;
657 for ( n = 0; n < len; n += 1 ) {
658 if ( $.inArray( deps[n], resolved ) === -1 ) {
659 if ( unresolved[deps[n]] ) {
661 'Circular reference detected: ' + module +
667 unresolved[module] = true;
668 sortDependencies( deps[n], resolved, unresolved );
669 delete unresolved[module];
672 resolved[resolved.length] = module;
676 * Gets a list of module names that a module depends on in their proper dependency
680 * @param {string} module Module name or array of string module names
681 * @return {Array} list of dependencies, including 'module'.
682 * @throws {Error} If circular reference is detected
684 function resolve( module ) {
687 // Allow calling with an array of module names
688 if ( $.isArray( module ) ) {
690 for ( m = 0; m < module.length; m += 1 ) {
691 sortDependencies( module[m], resolved );
696 if ( typeof module === 'string' ) {
698 sortDependencies( module, resolved );
702 throw new Error( 'Invalid module argument: ' + module );
706 * Narrows a list of module names down to those matching a specific
707 * state (see comment on top of this scope for a list of valid states).
708 * One can also filter for 'unregistered', which will return the
709 * modules names that don't have a registry entry.
712 * @param {string|string[]} states Module states to filter by
713 * @param {Array} [modules] List of module names to filter (optional, by default the entire
715 * @return {Array} List of filtered module names
717 function filter( states, modules ) {
718 var list, module, s, m;
720 // Allow states to be given as a string
721 if ( typeof states === 'string' ) {
724 // If called without a list of modules, build and use a list of all modules
726 if ( modules === undefined ) {
728 for ( module in registry ) {
729 modules[modules.length] = module;
732 // Build a list of modules which are in one of the specified states
733 for ( s = 0; s < states.length; s += 1 ) {
734 for ( m = 0; m < modules.length; m += 1 ) {
735 if ( registry[modules[m]] === undefined ) {
736 // Module does not exist
737 if ( states[s] === 'unregistered' ) {
739 list[list.length] = modules[m];
742 // Module exists, check state
743 if ( registry[modules[m]].state === states[s] ) {
745 list[list.length] = modules[m];
754 * Determine whether all dependencies are in state 'ready', which means we may
755 * execute the module or job now.
758 * @param {Array} dependencies Dependencies (module names) to be checked.
759 * @return {boolean} True if all dependencies are in state 'ready', false otherwise
761 function allReady( dependencies ) {
762 return filter( 'ready', dependencies ).length === dependencies.length;
766 * Log a message to window.console, if possible. Useful to force logging of some
767 * errors that are otherwise hard to detect (I.e., this logs also in production mode).
768 * Gets console references in each invocation, so that delayed debugging tools work
769 * fine. No need for optimization here, which would only result in losing logs.
772 * @param {string} msg text for the log entry.
775 function log( msg, e ) {
776 var console = window.console;
777 if ( console && console.log ) {
779 // If we have an exception object, log it through .error() to trigger
780 // proper stacktraces in browsers that support it. There are no (known)
781 // browsers that don't support .error(), that do support .log() and
782 // have useful exception handling through .log().
783 if ( e && console.error ) {
790 * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs
791 * and modules that depend upon this module. if the given module failed, propagate the 'error'
792 * state up the dependency tree; otherwise, execute all jobs/modules that now have all their
793 * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any.
796 * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
798 function handlePending( module ) {
799 var j, job, hasErrors, m, stateChange;
802 if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) {
803 // If the current module failed, mark all dependent modules also as failed.
804 // Iterate until steady-state to propagate the error state upwards in the
808 for ( m in registry ) {
809 if ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) {
810 if ( filter( ['error', 'missing'], registry[m].dependencies ).length > 0 ) {
811 registry[m].state = 'error';
816 } while ( stateChange );
819 // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
820 for ( j = 0; j < jobs.length; j += 1 ) {
821 hasErrors = filter( ['error', 'missing'], jobs[j].dependencies ).length > 0;
822 if ( hasErrors || allReady( jobs[j].dependencies ) ) {
823 // All dependencies satisfied, or some have errors
829 throw new Error( 'Module ' + module + ' failed.');
831 if ( $.isFunction( job.ready ) ) {
836 if ( $.isFunction( job.error ) ) {
838 job.error( e, [module] );
840 // A user-defined operation raised an exception. Swallow to protect
841 // our state machine!
842 log( 'Exception thrown by job.error()', ex );
849 if ( registry[module].state === 'ready' ) {
850 // The current module became 'ready'. Recursively execute all dependent modules that are loaded
851 // and now have all dependencies satisfied.
852 for ( m in registry ) {
853 if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) {
861 * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
862 * depending on whether document-ready has occurred yet and whether we are in async mode.
865 * @param {string} src URL to script, will be used as the src attribute in the script tag
866 * @param {Function} [callback] Callback which will be run when the script is done
868 function addScript( src, callback, async ) {
869 /*jshint evil:true */
870 var script, head, done;
872 // Using isReady directly instead of storing it locally from
873 // a $.fn.ready callback (bug 31895).
874 if ( $.isReady || async ) {
875 // Can't use jQuery.getScript because that only uses <script> for cross-domain,
876 // it uses XHR and eval for same-domain scripts, which we don't want because it
877 // messes up line numbers.
878 // The below is based on jQuery ([jquery@1.8.2]/src/ajax/script.js)
880 // IE-safe way of getting the <head>. document.head isn't supported
881 // in old IE, and doesn't work when in the <head>.
883 head = document.getElementsByTagName( 'head' )[0] || document.body;
885 script = document.createElement( 'script' );
888 if ( $.isFunction( callback ) ) {
889 script.onload = script.onreadystatechange = function () {
894 || /loaded|complete/.test( script.readyState )
899 // Handle memory leak in IE
900 script.onload = script.onreadystatechange = null;
902 // Detach the element from the document
903 if ( script.parentNode ) {
904 script.parentNode.removeChild( script );
907 // Dereference the element from javascript
915 if ( window.opera ) {
916 // Appending to the <head> blocks rendering completely in Opera,
917 // so append to the <body> after document ready. This means the
918 // scripts only start loading after the document has been rendered,
919 // but so be it. Opera users don't deserve faster web pages if their
920 // browser makes it impossible.
922 document.body.appendChild( script );
925 head.appendChild( script );
928 document.write( mw.html.element( 'script', { 'src': src }, '' ) );
929 if ( $.isFunction( callback ) ) {
930 // Document.write is synchronous, so this is called when it's done
931 // FIXME: that's a lie. doc.write isn't actually synchronous
938 * Executes a loaded module, making it ready to use
941 * @param {string} module Module name to execute
943 function execute( module ) {
944 var key, value, media, i, urls, cssHandle, checkCssHandles,
945 cssHandlesRegistered = false;
947 if ( registry[module] === undefined ) {
948 throw new Error( 'Module has not been registered yet: ' + module );
949 } else if ( registry[module].state === 'registered' ) {
950 throw new Error( 'Module has not been requested from the server yet: ' + module );
951 } else if ( registry[module].state === 'loading' ) {
952 throw new Error( 'Module has not completed loading yet: ' + module );
953 } else if ( registry[module].state === 'ready' ) {
954 throw new Error( 'Module has already been executed: ' + module );
958 * Define loop-function here for efficiency
959 * and to avoid re-using badly scoped variables.
962 function addLink( media, url ) {
963 var el = document.createElement( 'link' );
964 getMarker().before( el ); // IE: Insert in dom before setting href
965 el.rel = 'stylesheet';
966 if ( media && media !== 'all' ) {
972 function runScript() {
973 var script, markModuleReady, nestedAddScript;
975 script = registry[module].script;
976 markModuleReady = function () {
977 registry[module].state = 'ready';
978 handlePending( module );
980 nestedAddScript = function ( arr, callback, async, i ) {
981 // Recursively call addScript() in its own callback
982 // for each element of arr.
983 if ( i >= arr.length ) {
984 // We're at the end of the array
989 addScript( arr[i], function () {
990 nestedAddScript( arr, callback, async, i + 1 );
994 if ( $.isArray( script ) ) {
995 nestedAddScript( script, markModuleReady, registry[module].async, 0 );
996 } else if ( $.isFunction( script ) ) {
997 registry[module].state = 'ready';
999 handlePending( module );
1002 // This needs to NOT use mw.log because these errors are common in production mode
1003 // and not in debug mode, such as when a symbol that should be global isn't exported
1004 log( 'Exception thrown by ' + module + ': ' + e.message, e );
1005 registry[module].state = 'error';
1006 handlePending( module );
1010 // This used to be inside runScript, but since that is now fired asychronously
1011 // (after CSS is loaded) we need to set it here right away. It is crucial that
1012 // when execute() is called this is set synchronously, otherwise modules will get
1013 // executed multiple times as the registry will state that it isn't loading yet.
1014 registry[module].state = 'loading';
1016 // Add localizations to message system
1017 if ( $.isPlainObject( registry[module].messages ) ) {
1018 mw.messages.set( registry[module].messages );
1021 // Make sure we don't run the scripts until all (potentially asynchronous)
1022 // stylesheet insertions have completed.
1025 checkCssHandles = function () {
1026 // cssHandlesRegistered ensures we don't take off too soon, e.g. when
1027 // one of the cssHandles is fired while we're still creating more handles.
1028 if ( cssHandlesRegistered && pending === 0 && runScript ) {
1030 runScript = undefined; // Revoke
1033 cssHandle = function () {
1034 var check = checkCssHandles;
1036 return function () {
1040 check = undefined; // Revoke
1046 // Process styles (see also mw.loader.implement)
1047 // * back-compat: { <media>: css }
1048 // * back-compat: { <media>: [url, ..] }
1049 // * { "css": [css, ..] }
1050 // * { "url": { <media>: [url, ..] } }
1051 if ( $.isPlainObject( registry[module].style ) ) {
1052 for ( key in registry[module].style ) {
1053 value = registry[module].style[key];
1056 if ( key !== 'url' && key !== 'css' ) {
1057 // Backwards compatibility, key is a media-type
1058 if ( typeof value === 'string' ) {
1059 // back-compat: { <media>: css }
1060 // Ignore 'media' because it isn't supported (nor was it used).
1061 // Strings are pre-wrapped in "@media". The media-type was just ""
1062 // (because it had to be set to something).
1063 // This is one of the reasons why this format is no longer used.
1064 addEmbeddedCSS( value, cssHandle() );
1066 // back-compat: { <media>: [url, ..] }
1072 // Array of css strings in key 'css',
1073 // or back-compat array of urls from media-type
1074 if ( $.isArray( value ) ) {
1075 for ( i = 0; i < value.length; i += 1 ) {
1076 if ( key === 'bc-url' ) {
1077 // back-compat: { <media>: [url, ..] }
1078 addLink( media, value[i] );
1079 } else if ( key === 'css' ) {
1080 // { "css": [css, ..] }
1081 addEmbeddedCSS( value[i], cssHandle() );
1084 // Not an array, but a regular object
1085 // Array of urls inside media-type key
1086 } else if ( typeof value === 'object' ) {
1087 // { "url": { <media>: [url, ..] } }
1088 for ( media in value ) {
1089 urls = value[media];
1090 for ( i = 0; i < urls.length; i += 1 ) {
1091 addLink( media, urls[i] );
1099 cssHandlesRegistered = true;
1104 * Adds a dependencies to the queue with optional callbacks to be run
1105 * when the dependencies are ready or fail
1108 * @param {string|string[]} dependencies Module name or array of string module names
1109 * @param {Function} [ready] Callback to execute when all dependencies are ready
1110 * @param {Function} [error] Callback to execute when any dependency fails
1111 * @param {boolean} [async] If true, load modules asynchronously even if
1112 * document ready has not yet occurred.
1114 function request( dependencies, ready, error, async ) {
1117 // Allow calling by single module name
1118 if ( typeof dependencies === 'string' ) {
1119 dependencies = [dependencies];
1122 // Add ready and error callbacks if they were given
1123 if ( ready !== undefined || error !== undefined ) {
1124 jobs[jobs.length] = {
1125 'dependencies': filter(
1126 ['registered', 'loading', 'loaded'],
1134 // Queue up any dependencies that are registered
1135 dependencies = filter( ['registered'], dependencies );
1136 for ( n = 0; n < dependencies.length; n += 1 ) {
1137 if ( $.inArray( dependencies[n], queue ) === -1 ) {
1138 queue[queue.length] = dependencies[n];
1140 // Mark this module as async in the registry
1141 registry[dependencies[n]].async = true;
1150 function sortQuery(o) {
1151 var sorted = {}, key, a = [];
1153 if ( hasOwn.call( o, key ) ) {
1158 for ( key = 0; key < a.length; key += 1 ) {
1159 sorted[a[key]] = o[a[key]];
1165 * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
1166 * to a query string of the form foo.bar,baz|bar.baz,quux
1169 function buildModulesString( moduleMap ) {
1170 var arr = [], p, prefix;
1171 for ( prefix in moduleMap ) {
1172 p = prefix === '' ? '' : prefix + '.';
1173 arr.push( p + moduleMap[prefix].join( ',' ) );
1175 return arr.join( '|' );
1179 * Asynchronously append a script tag to the end of the body
1180 * that invokes load.php
1182 * @param {Object} moduleMap Module map, see #buildModulesString
1183 * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
1184 * @param {string} sourceLoadScript URL of load.php
1185 * @param {boolean} async If true, use an asynchrounous request even if document ready has not yet occurred
1187 function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) {
1188 var request = $.extend(
1189 { modules: buildModulesString( moduleMap ) },
1192 request = sortQuery( request );
1193 // Asynchronously append a script tag to the end of the body
1194 // Append &* to avoid triggering the IE6 extension check
1195 addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
1198 /* Public Methods */
1201 * @inheritdoc #newStyleTag
1204 addStyleTag: newStyleTag,
1207 * Batch-request queued dependencies from the server.
1210 var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
1211 source, group, g, i, modules, maxVersion, sourceLoadScript,
1212 currReqBase, currReqBaseLength, moduleMap, l,
1213 lastDotIndex, prefix, suffix, bytesAdded, async;
1215 // Build a list of request parameters common to all requests.
1217 skin: mw.config.get( 'skin' ),
1218 lang: mw.config.get( 'wgUserLanguage' ),
1219 debug: mw.config.get( 'debug' )
1221 // Split module batch by source and by group.
1223 maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 );
1225 // Appends a list of modules from the queue to the batch
1226 for ( q = 0; q < queue.length; q += 1 ) {
1227 // Only request modules which are registered
1228 if ( registry[queue[q]] !== undefined && registry[queue[q]].state === 'registered' ) {
1229 // Prevent duplicate entries
1230 if ( $.inArray( queue[q], batch ) === -1 ) {
1231 batch[batch.length] = queue[q];
1232 // Mark registered modules as loading
1233 registry[queue[q]].state = 'loading';
1237 // Early exit if there's nothing to load...
1238 if ( !batch.length ) {
1242 // The queue has been processed into the batch, clear up the queue.
1245 // Always order modules alphabetically to help reduce cache
1246 // misses for otherwise identical content.
1249 // Split batch by source and by group.
1250 for ( b = 0; b < batch.length; b += 1 ) {
1251 bSource = registry[batch[b]].source;
1252 bGroup = registry[batch[b]].group;
1253 if ( splits[bSource] === undefined ) {
1254 splits[bSource] = {};
1256 if ( splits[bSource][bGroup] === undefined ) {
1257 splits[bSource][bGroup] = [];
1259 bSourceGroup = splits[bSource][bGroup];
1260 bSourceGroup[bSourceGroup.length] = batch[b];
1263 // Clear the batch - this MUST happen before we append any
1264 // script elements to the body or it's possible that a script
1265 // will be locally cached, instantly load, and work the batch
1266 // again, all before we've cleared it causing each request to
1267 // include modules which are already loaded.
1270 for ( source in splits ) {
1272 sourceLoadScript = sources[source].loadScript;
1274 for ( group in splits[source] ) {
1276 // Cache access to currently selected list of
1277 // modules for this group from this source.
1278 modules = splits[source][group];
1280 // Calculate the highest timestamp
1282 for ( g = 0; g < modules.length; g += 1 ) {
1283 if ( registry[modules[g]].version > maxVersion ) {
1284 maxVersion = registry[modules[g]].version;
1288 currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase );
1289 // For user modules append a user name to the request.
1290 if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
1291 currReqBase.user = mw.config.get( 'wgUserName' );
1293 currReqBaseLength = $.param( currReqBase ).length;
1295 // We may need to split up the request to honor the query string length limit,
1296 // so build it piece by piece.
1297 l = currReqBaseLength + 9; // '&modules='.length == 9
1299 moduleMap = {}; // { prefix: [ suffixes ] }
1301 for ( i = 0; i < modules.length; i += 1 ) {
1302 // Determine how many bytes this module would add to the query string
1303 lastDotIndex = modules[i].lastIndexOf( '.' );
1304 // Note that these substr() calls work even if lastDotIndex == -1
1305 prefix = modules[i].substr( 0, lastDotIndex );
1306 suffix = modules[i].substr( lastDotIndex + 1 );
1307 bytesAdded = moduleMap[prefix] !== undefined
1308 ? suffix.length + 3 // '%2C'.length == 3
1309 : modules[i].length + 3; // '%7C'.length == 3
1311 // If the request would become too long, create a new one,
1312 // but don't create empty requests
1313 if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
1314 // This request would become too long, create a new one
1315 // and fire off the old one
1316 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1319 l = currReqBaseLength + 9;
1321 if ( moduleMap[prefix] === undefined ) {
1322 moduleMap[prefix] = [];
1324 moduleMap[prefix].push( suffix );
1325 if ( !registry[modules[i]].async ) {
1326 // If this module is blocking, make the entire request blocking
1327 // This is slightly suboptimal, but in practice mixing of blocking
1328 // and async modules will only occur in debug mode.
1333 // If there's anything left in moduleMap, request that too
1334 if ( !$.isEmptyObject( moduleMap ) ) {
1335 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1342 * Register a source.
1344 * @param {string} id Short lowercase a-Z string representing a source, only used internally.
1345 * @param {Object} props Object containing only the loadScript property which is a url to
1346 * the load.php location of the source.
1349 addSource: function ( id, props ) {
1351 // Allow multiple additions
1352 if ( typeof id === 'object' ) {
1353 for ( source in id ) {
1354 mw.loader.addSource( source, id[source] );
1359 if ( sources[id] !== undefined ) {
1360 throw new Error( 'source already registered: ' + id );
1363 sources[id] = props;
1369 * Register a module, letting the system know about it and its
1370 * properties. Startup modules contain calls to this function.
1372 * @param {string} module Module name
1373 * @param {number} version Module version number as a timestamp (falls backs to 0)
1374 * @param {string|Array|Function} dependencies One string or array of strings of module
1375 * names on which this module depends, or a function that returns that array.
1376 * @param {string} [group=null] Group which the module is in
1377 * @param {string} [source='local'] Name of the source
1379 register: function ( module, version, dependencies, group, source ) {
1381 // Allow multiple registration
1382 if ( typeof module === 'object' ) {
1383 for ( m = 0; m < module.length; m += 1 ) {
1384 // module is an array of module names
1385 if ( typeof module[m] === 'string' ) {
1386 mw.loader.register( module[m] );
1387 // module is an array of arrays
1388 } else if ( typeof module[m] === 'object' ) {
1389 mw.loader.register.apply( mw.loader, module[m] );
1395 if ( typeof module !== 'string' ) {
1396 throw new Error( 'module must be a string, not a ' + typeof module );
1398 if ( registry[module] !== undefined ) {
1399 throw new Error( 'module already registered: ' + module );
1401 // List the module as registered
1402 registry[module] = {
1403 version: version !== undefined ? parseInt( version, 10 ) : 0,
1405 group: typeof group === 'string' ? group : null,
1406 source: typeof source === 'string' ? source: 'local',
1409 if ( typeof dependencies === 'string' ) {
1410 // Allow dependencies to be given as a single module name
1411 registry[module].dependencies = [ dependencies ];
1412 } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
1413 // Allow dependencies to be given as an array of module names
1414 // or a function which returns an array
1415 registry[module].dependencies = dependencies;
1420 * Implement a module given the components that make up the module.
1422 * When #load or #using requests one or more modules, the server
1423 * response contain calls to this function.
1425 * All arguments are required.
1427 * @param {string} module Name of module
1428 * @param {Function|Array} script Function with module code or Array of URLs to
1429 * be used as the src attribute of a new `<script>` tag.
1430 * @param {Object} style Should follow one of the following patterns:
1431 * { "css": [css, ..] }
1432 * { "url": { <media>: [url, ..] } }
1433 * And for backwards compatibility (needs to be supported forever due to caching):
1435 * { <media>: [url, ..] }
1437 * The reason css strings are not concatenated anymore is bug 31676. We now check
1438 * whether it's safe to extend the stylesheet (see #canExpandStylesheetWith).
1440 * @param {Object} msgs List of key/value pairs to be added to {@link mw#messages}.
1442 implement: function ( module, script, style, msgs ) {
1444 if ( typeof module !== 'string' ) {
1445 throw new Error( 'module must be a string, not a ' + typeof module );
1447 if ( !$.isFunction( script ) && !$.isArray( script ) ) {
1448 throw new Error( 'script must be a function or an array, not a ' + typeof script );
1450 if ( !$.isPlainObject( style ) ) {
1451 throw new Error( 'style must be an object, not a ' + typeof style );
1453 if ( !$.isPlainObject( msgs ) ) {
1454 throw new Error( 'msgs must be an object, not a ' + typeof msgs );
1456 // Automatically register module
1457 if ( registry[module] === undefined ) {
1458 mw.loader.register( module );
1460 // Check for duplicate implementation
1461 if ( registry[module] !== undefined && registry[module].script !== undefined ) {
1462 throw new Error( 'module already implemented: ' + module );
1464 // Attach components
1465 registry[module].script = script;
1466 registry[module].style = style;
1467 registry[module].messages = msgs;
1468 // The module may already have been marked as erroneous
1469 if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
1470 registry[module].state = 'loaded';
1471 if ( allReady( registry[module].dependencies ) ) {
1478 * Execute a function as soon as one or more required modules are ready.
1480 * @param {string|Array} dependencies Module name or array of modules names the callback
1481 * dependends on to be ready before executing
1482 * @param {Function} [ready] callback to execute when all dependencies are ready
1483 * @param {Function} [error] callback to execute when if dependencies have a errors
1485 using: function ( dependencies, ready, error ) {
1486 var tod = typeof dependencies;
1488 if ( tod !== 'object' && tod !== 'string' ) {
1489 throw new Error( 'dependencies must be a string or an array, not a ' + tod );
1491 // Allow calling with a single dependency as a string
1492 if ( tod === 'string' ) {
1493 dependencies = [ dependencies ];
1495 // Resolve entire dependency map
1496 dependencies = resolve( dependencies );
1497 if ( allReady( dependencies ) ) {
1498 // Run ready immediately
1499 if ( $.isFunction( ready ) ) {
1502 } else if ( filter( ['error', 'missing'], dependencies ).length ) {
1503 // Execute error immediately if any dependencies have errors
1504 if ( $.isFunction( error ) ) {
1505 error( new Error( 'one or more dependencies have state "error" or "missing"' ),
1509 // Not all dependencies are ready: queue up a request
1510 request( dependencies, ready, error );
1515 * Load an external script or one or more modules.
1517 * @param {string|Array} modules Either the name of a module, array of modules,
1518 * or a URL of an external script or style
1519 * @param {string} [type='text/javascript'] mime-type to use if calling with a URL of an
1520 * external script or style; acceptable values are "text/css" and
1521 * "text/javascript"; if no type is provided, text/javascript is assumed.
1522 * @param {boolean} [async] If true, load modules asynchronously
1523 * even if document ready has not yet occurred. If false, block before
1524 * document ready and load async after. If not set, true will be
1525 * assumed if loading a URL, and false will be assumed otherwise.
1527 load: function ( modules, type, async ) {
1528 var filtered, m, module, l;
1531 if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
1532 throw new Error( 'modules must be a string or an array, not a ' + typeof modules );
1534 // Allow calling with an external url or single dependency as a string
1535 if ( typeof modules === 'string' ) {
1536 // Support adding arbitrary external scripts
1537 if ( /^(https?:)?\/\//.test( modules ) ) {
1538 if ( async === undefined ) {
1539 // Assume async for bug 34542
1542 if ( type === 'text/css' ) {
1543 // IE7-8 throws security warnings when inserting a <link> tag
1544 // with a protocol-relative URL set though attributes (instead of
1545 // properties) - when on HTTPS. See also bug #.
1546 l = document.createElement( 'link' );
1547 l.rel = 'stylesheet';
1549 $( 'head' ).append( l );
1552 if ( type === 'text/javascript' || type === undefined ) {
1553 addScript( modules, null, async );
1557 throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
1559 // Called with single module
1560 modules = [ modules ];
1563 // Filter out undefined modules, otherwise resolve() will throw
1564 // an exception for trying to load an undefined module.
1565 // Undefined modules are acceptable here in load(), because load() takes
1566 // an array of unrelated modules, whereas the modules passed to
1567 // using() are related and must all be loaded.
1568 for ( filtered = [], m = 0; m < modules.length; m += 1 ) {
1569 module = registry[modules[m]];
1570 if ( module !== undefined ) {
1571 if ( $.inArray( module.state, ['error', 'missing'] ) === -1 ) {
1572 filtered[filtered.length] = modules[m];
1577 if ( filtered.length === 0 ) {
1580 // Resolve entire dependency map
1581 filtered = resolve( filtered );
1582 // If all modules are ready, nothing to be done
1583 if ( allReady( filtered ) ) {
1586 // If any modules have errors: also quit.
1587 if ( filter( ['error', 'missing'], filtered ).length ) {
1590 // Since some modules are not yet ready, queue up a request.
1591 request( filtered, undefined, undefined, async );
1595 * Change the state of one or more modules.
1597 * @param {string|Object} module module name or object of module name/state pairs
1598 * @param {string} state state name
1600 state: function ( module, state ) {
1603 if ( typeof module === 'object' ) {
1604 for ( m in module ) {
1605 mw.loader.state( m, module[m] );
1609 if ( registry[module] === undefined ) {
1610 mw.loader.register( module );
1612 if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1
1613 && registry[module].state !== state ) {
1614 // Make sure pending modules depending on this one get executed if their
1615 // dependencies are now fulfilled!
1616 registry[module].state = state;
1617 handlePending( module );
1619 registry[module].state = state;
1624 * Get the version of a module.
1626 * @param {string} module Name of module to get version for
1628 getVersion: function ( module ) {
1629 if ( registry[module] !== undefined && registry[module].version !== undefined ) {
1630 return formatVersionNumber( registry[module].version );
1636 * @inheritdoc #getVersion
1637 * @deprecated since 1.18 use #getVersion instead
1639 version: function () {
1640 return mw.loader.getVersion.apply( mw.loader, arguments );
1644 * Get the state of a module.
1646 * @param {string} module name of module to get state for
1648 getState: function ( module ) {
1649 if ( registry[module] !== undefined && registry[module].state !== undefined ) {
1650 return registry[module].state;
1656 * Get names of all registered modules.
1660 getModuleNames: function () {
1661 return $.map( registry, function ( i, key ) {
1667 * Load the `mediawiki.user` module.
1669 * For backwards-compatibility with cached pages from before 2013 where:
1671 * - the `mediawiki.user` module didn't exist yet
1672 * - `mw.user` was still part of mediawiki.js
1673 * - `mw.loader.go` still existed and called after `mw.loader.load()`
1676 mw.loader.load( 'mediawiki.user' );
1682 * HTML construction helper functions
1689 * output = Html.element( 'div', {}, new Html.Raw(
1690 * Html.element( 'img', { src: '<' } )
1692 * mw.log( output ); // <div><img src="<"/></div>
1697 html: ( function () {
1698 function escapeCallback( s ) {
1715 * Escape a string for HTML. Converts special characters to HTML entities.
1716 * @param {string} s The string to escape
1718 escape: function ( s ) {
1719 return s.replace( /['"<>&]/g, escapeCallback );
1723 * Create an HTML element string, with safe escaping.
1725 * @param {string} name The tag name.
1726 * @param {Object} attrs An object with members mapping element names to values
1727 * @param {Mixed} contents The contents of the element. May be either:
1728 * - string: The string is escaped.
1729 * - null or undefined: The short closing form is used, e.g. <br/>.
1730 * - this.Raw: The value attribute is included without escaping.
1731 * - this.Cdata: The value attribute is included, and an exception is
1732 * thrown if it contains an illegal ETAGO delimiter.
1733 * See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2
1735 element: function ( name, attrs, contents ) {
1736 var v, attrName, s = '<' + name;
1738 for ( attrName in attrs ) {
1739 v = attrs[attrName];
1740 // Convert name=true, to name=name
1744 } else if ( v === false ) {
1747 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
1749 if ( contents === undefined || contents === null ) {
1756 switch ( typeof contents ) {
1759 s += this.escape( contents );
1763 // Convert to string
1764 s += String( contents );
1767 if ( contents instanceof this.Raw ) {
1768 // Raw HTML inclusion
1769 s += contents.value;
1770 } else if ( contents instanceof this.Cdata ) {
1772 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
1773 throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
1775 s += contents.value;
1777 throw new Error( 'mw.html.element: Invalid type of contents' );
1780 s += '</' + name + '>';
1785 * Wrapper object for raw HTML passed to mw.html.element().
1786 * @class mw.html.Raw
1788 Raw: function ( value ) {
1793 * Wrapper object for CDATA element contents passed to mw.html.element()
1794 * @class mw.html.Cdata
1796 Cdata: function ( value ) {
1802 // Skeleton user object. mediawiki.user.js extends this
1809 * Registry and firing of events.
1811 * MediaWiki has various interface components that are extended, enhanced
1812 * or manipulated in some other way by extensions, gadgets and even
1815 * This framework helps streamlining the timing of when these other
1816 * code paths fire their plugins (instead of using document-ready,
1817 * which can and should be limited to firing only once).
1819 * Features like navigating to other wiki pages, previewing an edit
1820 * and editing itself – without a refresh – can then retrigger these
1821 * hooks accordingly to ensure everything still works as expected.
1825 * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
1826 * mw.hook( 'wikipage.content' ).fire( $content );
1828 * Handlers can be added and fired for arbitrary event names at any time. The same
1829 * event can be fired multiple times. The last run of an event is memorized
1830 * (similar to `$(document).ready` and `$.Deferred().done`).
1831 * This means if an event is fired, and a handler added afterwards, the added
1832 * function will be fired right away with the last given event data.
1834 * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
1835 * Thus allowing flexible use and optimal maintainability and authority control.
1836 * You can pass around the `add` and/or `fire` method to another piece of code
1837 * without it having to know the event name (or `mw.hook` for that matter).
1839 * var h = mw.hook( 'bar.ready' );
1840 * new mw.Foo( .. ).fetch( { callback: h.fire } );
1842 * Note: Events are documented with an underscore instead of a dot in the event
1843 * name due to jsduck not supporting dots in that position.
1847 hook: ( function () {
1851 * Create an instance of mw.hook.
1855 * @param {string} name Name of hook.
1858 return function ( name ) {
1859 var list = lists[name] || ( lists[name] = $.Callbacks( 'memory' ) );
1863 * Register a hook handler
1864 * @param {Function...} handler Function to bind.
1870 * Unregister a hook handler
1871 * @param {Function...} handler Function to unbind.
1874 remove: list.remove,
1878 * @param {Mixed...} data
1882 return list.fireWith( null, slice.call( arguments ) );
1891 // Alias $j to jQuery for backwards compatibility
1894 // Attach to window and globally alias
1895 window.mw = window.mediaWiki = mw;
1897 // Auto-register from pre-loaded startup scripts
1898 if ( jQuery.isFunction( window.startUp ) ) {
1900 window.startUp = undefined;