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.
148 * Similar to the Message class in MediaWiki PHP.
150 * Format defaults to 'text'.
155 * @param {mw.Map} map Message storage
156 * @param {string} key
157 * @param {Array} [parameters]
159 function Message( map, key, parameters ) {
160 this.format = 'text';
163 this.parameters = parameters === undefined ? [] : slice.call( parameters );
167 Message.prototype = {
169 * Simple message parser, does $N replacement and nothing else.
171 * This may be overridden to provide a more complex message parser.
173 * The primary override is in mediawiki.jqueryMsg.
175 * This function will not be called for nonexistent messages.
177 parser: function () {
178 var parameters = this.parameters;
179 return this.map.get( this.key ).replace( /\$(\d+)/g, function ( str, match ) {
180 var index = parseInt( match, 10 ) - 1;
181 return parameters[index] !== undefined ? parameters[index] : '$' + match;
186 * Appends (does not replace) parameters for replacement to the .parameters property.
188 * @param {Array} parameters
191 params: function ( parameters ) {
193 for ( i = 0; i < parameters.length; i += 1 ) {
194 this.parameters.push( parameters[i] );
200 * Converts message object to it's string form based on the state of format.
202 * @return {string} Message as a string in the current form or `<key>` if key does not exist.
204 toString: function () {
207 if ( !this.exists() ) {
208 // Use <key> as text if key does not exist
209 if ( this.format === 'escaped' || this.format === 'parse' ) {
210 // format 'escaped' and 'parse' need to have the brackets and key html escaped
211 return mw.html.escape( '<' + this.key + '>' );
213 return '<' + this.key + '>';
216 if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) {
217 text = this.parser();
220 if ( this.format === 'escaped' ) {
221 text = this.parser();
222 text = mw.html.escape( text );
229 * Changes format to 'parse' and converts message to string
231 * If jqueryMsg is loaded, this parses the message text from wikitext
232 * (where supported) to HTML
234 * Otherwise, it is equivalent to plain.
236 * @return {string} String form of parsed message
239 this.format = 'parse';
240 return this.toString();
244 * Changes format to 'plain' and converts message to string
246 * This substitutes parameters, but otherwise does not change the
249 * @return {string} String form of plain message
252 this.format = 'plain';
253 return this.toString();
257 * Changes format to 'text' and converts message to string
259 * If jqueryMsg is loaded, {{-transformation is done where supported
260 * (such as {{plural:}}, {{gender:}}, {{int:}}).
262 * Otherwise, it is equivalent to plain.
265 this.format = 'text';
266 return this.toString();
270 * Changes the format to 'escaped' and converts message to string
272 * This is equivalent to using the 'text' format (see text method), then
273 * HTML-escaping the output.
275 * @return {string} String form of html escaped message
277 escaped: function () {
278 this.format = 'escaped';
279 return this.toString();
283 * Checks if message exists
288 exists: function () {
289 return this.map.exists( this.key );
294 * Base library for MediaWiki.
297 * @alternateClassName mediaWiki
304 * Dummy placeholder for {@link mw.log}
308 var log = function () {};
309 log.warn = function () {};
310 log.deprecate = function ( obj, key, val ) {
316 // Make the Map constructor publicly available.
319 // Make the Message constructor publicly available.
323 * Map of configuration values
325 * Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config)
328 * If `$wgLegacyJavaScriptGlobals` is true, this Map will put its values in the
329 * global window object.
331 * @property {mw.Map} config
333 // Dummy placeholder. Re-assigned in ResourceLoaderStartupModule with an instance of `mw.Map`.
337 * Empty object that plugins can be installed in.
343 * Access container for deprecated functionality that can be moved from
344 * from their legacy location and attached to this object (e.g. a global
345 * function that is deprecated and as stop-gap can be exposed through here).
347 * This was reserved for future use but never ended up being used.
349 * @deprecated since 1.22: Let deprecated identifiers keep their original name
350 * and use mw.log#deprecate to create an access container for tracking.
356 * Localization system
364 * Get a message object.
366 * Similar to wfMessage() in MediaWiki PHP.
368 * @param {string} key Key of message to get
369 * @param {Mixed...} parameters Parameters for the $N replacements in messages.
370 * @return {mw.Message}
372 message: function ( key ) {
373 // Variadic arguments
374 var parameters = slice.call( arguments, 1 );
375 return new Message( mw.messages, key, parameters );
379 * Get a message string using 'text' format.
381 * Similar to wfMsg() in MediaWiki PHP.
384 * @param {string} key Key of message to get
385 * @param {Mixed...} parameters Parameters for the $N replacements in messages.
389 return mw.message.apply( mw.message, arguments ).toString();
393 * Client-side module loader which integrates with the MediaWiki ResourceLoader
397 loader: ( function () {
399 /* Private Members */
402 * Mapping of registered modules
404 * The jquery module is pre-registered, because it must have already
405 * been provided for this object to have been built, and in debug mode
406 * jquery would have been provided through a unique loader request,
407 * making it impossible to hold back registration of jquery until after
410 * For exact details on support for script, style and messages, look at
411 * mw.loader.implement.
416 * 'version': ############## (unix timestamp),
417 * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
418 * 'group': 'somegroup', (or) null,
419 * 'source': 'local', 'someforeignwiki', (or) null
420 * 'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing'
423 * 'messages': { 'key': 'value' },
432 // Mapping of sources, keyed by source-id, values are objects.
436 // 'loadScript': 'http://foo.bar/w/load.php'
441 // List of modules which will be loaded as when ready
443 // List of modules to be loaded
445 // List of callback functions waiting for modules to be ready to be called
447 // Selector cache for the marker element. Use getMarker() to get/use the marker!
449 // Buffer for addEmbeddedCSS.
451 // Callbacks for addEmbeddedCSS.
452 cssCallbacks = $.Callbacks();
454 /* Private methods */
456 function getMarker() {
462 $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
463 if ( $marker.length ) {
466 mw.log( 'getMarker> No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically.' );
467 $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' );
473 * Create a new style tag and add it to the DOM.
476 * @param {string} text CSS text
477 * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag should be
478 * inserted before. Otherwise it will be appended to `<head>`.
479 * @return {HTMLElement} Reference to the created `<style>` element.
481 function newStyleTag( text, nextnode ) {
482 var s = document.createElement( 'style' );
483 // Insert into document before setting cssText (bug 33305)
485 // Must be inserted with native insertBefore, not $.fn.before.
486 // When using jQuery to insert it, like $nextnode.before( s ),
487 // then IE6 will throw "Access is denied" when trying to append
488 // to .cssText later. Some kind of weird security measure.
489 // http://stackoverflow.com/q/12586482/319266
490 // Works: jsfiddle.net/zJzMy/1
491 // Fails: jsfiddle.net/uJTQz
492 // Works again: http://jsfiddle.net/Azr4w/ (diff: the next 3 lines)
493 if ( nextnode.jquery ) {
494 nextnode = nextnode.get( 0 );
496 nextnode.parentNode.insertBefore( s, nextnode );
498 document.getElementsByTagName( 'head' )[0].appendChild( s );
500 if ( s.styleSheet ) {
502 s.styleSheet.cssText = text;
505 // (Safari sometimes borks on non-string values,
506 // play safe by casting to a string, just in case.)
507 s.appendChild( document.createTextNode( String( text ) ) );
513 * Checks whether it is safe to add this css to a stylesheet.
516 * @param {string} cssText
517 * @return {boolean} False if a new one must be created.
519 function canExpandStylesheetWith( cssText ) {
520 // Makes sure that cssText containing `@import`
521 // rules will end up in a new stylesheet (as those only work when
522 // placed at the start of a stylesheet; bug 35562).
523 return cssText.indexOf( '@import' ) === -1;
527 * Add a bit of CSS text to the current browser page.
529 * The CSS will be appended to an existing ResourceLoader-created `<style>` tag
530 * or create a new one based on whether the given `cssText` is safe for extension.
532 * @param {string} [cssText=cssBuffer] If called without cssText,
533 * the internal buffer will be inserted instead.
534 * @param {Function} [callback]
536 function addEmbeddedCSS( cssText, callback ) {
540 cssCallbacks.add( callback );
543 // Yield once before inserting the <style> tag. There are likely
544 // more calls coming up which we can combine this way.
545 // Appending a stylesheet and waiting for the browser to repaint
546 // is fairly expensive, this reduces it (bug 45810)
548 // Be careful not to extend the buffer with css that needs a new stylesheet
549 if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) {
550 // Linebreak for somewhat distinguishable sections
551 // (the rl-cachekey comment separating each)
552 cssBuffer += '\n' + cssText;
553 // TODO: Use requestAnimationFrame in the future which will
554 // perform even better by not injecting styles while the browser
556 setTimeout( function () {
557 // Can't pass addEmbeddedCSS to setTimeout directly because Firefox
558 // (below version 13) has the non-standard behaviour of passing a
559 // numerical "lateness" value as first argument to this callback
560 // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/
566 // This is a delayed call and we got a buffer still
567 } else if ( cssBuffer ) {
571 // This is a delayed call, but buffer is already cleared by
572 // another delayed call.
576 // By default, always create a new <style>. Appending text
577 // to a <style> tag means the contents have to be re-parsed (bug 45810).
578 // Except, of course, in IE below 9, in there we default to
579 // re-using and appending to a <style> tag due to the
580 // IE stylesheet limit (bug 31676).
581 if ( 'documentMode' in document && document.documentMode <= 9 ) {
583 $style = getMarker().prev();
584 // Verify that the the element before Marker actually is a
585 // <style> tag and one that came from ResourceLoader
586 // (not some other style tag or even a `<meta>` or `<script>`).
587 if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) {
588 // There's already a dynamic <style> tag present and
589 // canExpandStylesheetWith() gave a green light to append more to it.
590 styleEl = $style.get( 0 );
591 if ( styleEl.styleSheet ) {
593 styleEl.styleSheet.cssText += cssText; // IE
595 log( 'addEmbeddedCSS fail\ne.message: ' + e.message, e );
598 styleEl.appendChild( document.createTextNode( String( cssText ) ) );
600 cssCallbacks.fire().empty();
605 $( newStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true );
607 cssCallbacks.fire().empty();
611 * Generates an ISO8601 "basic" string from a UNIX timestamp
614 function formatVersionNumber( timestamp ) {
616 function pad( a, b, c ) {
617 return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
619 d.setTime( timestamp * 1000 );
621 pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T',
622 pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z'
627 * Resolves dependencies and detects circular references.
630 * @param {string} module Name of the top-level module whose dependencies shall be
631 * resolved and sorted.
632 * @param {Array} resolved Returns a topological sort of the given module and its
633 * dependencies, such that later modules depend on earlier modules. The array
634 * contains the module names. If the array contains already some module names,
635 * this function appends its result to the pre-existing array.
636 * @param {Object} [unresolved] Hash used to track the current dependency
637 * chain; used to report loops in the dependency graph.
638 * @throws {Error} If any unregistered module or a dependency loop is encountered
640 function sortDependencies( module, resolved, unresolved ) {
643 if ( registry[module] === undefined ) {
644 throw new Error( 'Unknown dependency: ' + module );
646 // Resolves dynamic loader function and replaces it with its own results
647 if ( $.isFunction( registry[module].dependencies ) ) {
648 registry[module].dependencies = registry[module].dependencies();
649 // Ensures the module's dependencies are always in an array
650 if ( typeof registry[module].dependencies !== 'object' ) {
651 registry[module].dependencies = [registry[module].dependencies];
654 if ( $.inArray( module, resolved ) !== -1 ) {
655 // Module already resolved; nothing to do.
658 // unresolved is optional, supply it if not passed in
662 // Tracks down dependencies
663 deps = registry[module].dependencies;
665 for ( n = 0; n < len; n += 1 ) {
666 if ( $.inArray( deps[n], resolved ) === -1 ) {
667 if ( unresolved[deps[n]] ) {
669 'Circular reference detected: ' + module +
675 unresolved[module] = true;
676 sortDependencies( deps[n], resolved, unresolved );
677 delete unresolved[module];
680 resolved[resolved.length] = module;
684 * Gets a list of module names that a module depends on in their proper dependency
688 * @param {string} module Module name or array of string module names
689 * @return {Array} list of dependencies, including 'module'.
690 * @throws {Error} If circular reference is detected
692 function resolve( module ) {
695 // Allow calling with an array of module names
696 if ( $.isArray( module ) ) {
698 for ( m = 0; m < module.length; m += 1 ) {
699 sortDependencies( module[m], resolved );
704 if ( typeof module === 'string' ) {
706 sortDependencies( module, resolved );
710 throw new Error( 'Invalid module argument: ' + module );
714 * Narrows a list of module names down to those matching a specific
715 * state (see comment on top of this scope for a list of valid states).
716 * One can also filter for 'unregistered', which will return the
717 * modules names that don't have a registry entry.
720 * @param {string|string[]} states Module states to filter by
721 * @param {Array} [modules] List of module names to filter (optional, by default the entire
723 * @return {Array} List of filtered module names
725 function filter( states, modules ) {
726 var list, module, s, m;
728 // Allow states to be given as a string
729 if ( typeof states === 'string' ) {
732 // If called without a list of modules, build and use a list of all modules
734 if ( modules === undefined ) {
736 for ( module in registry ) {
737 modules[modules.length] = module;
740 // Build a list of modules which are in one of the specified states
741 for ( s = 0; s < states.length; s += 1 ) {
742 for ( m = 0; m < modules.length; m += 1 ) {
743 if ( registry[modules[m]] === undefined ) {
744 // Module does not exist
745 if ( states[s] === 'unregistered' ) {
747 list[list.length] = modules[m];
750 // Module exists, check state
751 if ( registry[modules[m]].state === states[s] ) {
753 list[list.length] = modules[m];
762 * Determine whether all dependencies are in state 'ready', which means we may
763 * execute the module or job now.
766 * @param {Array} dependencies Dependencies (module names) to be checked.
767 * @return {boolean} True if all dependencies are in state 'ready', false otherwise
769 function allReady( dependencies ) {
770 return filter( 'ready', dependencies ).length === dependencies.length;
774 * Log a message to window.console, if possible. Useful to force logging of some
775 * errors that are otherwise hard to detect (I.e., this logs also in production mode).
776 * Gets console references in each invocation, so that delayed debugging tools work
777 * fine. No need for optimization here, which would only result in losing logs.
780 * @param {string} msg text for the log entry.
783 function log( msg, e ) {
784 var console = window.console;
785 if ( console && console.log ) {
787 // If we have an exception object, log it through .error() to trigger
788 // proper stacktraces in browsers that support it. There are no (known)
789 // browsers that don't support .error(), that do support .log() and
790 // have useful exception handling through .log().
791 if ( e && console.error ) {
798 * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs
799 * and modules that depend upon this module. if the given module failed, propagate the 'error'
800 * state up the dependency tree; otherwise, execute all jobs/modules that now have all their
801 * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any.
804 * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
806 function handlePending( module ) {
807 var j, job, hasErrors, m, stateChange;
810 if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) {
811 // If the current module failed, mark all dependent modules also as failed.
812 // Iterate until steady-state to propagate the error state upwards in the
816 for ( m in registry ) {
817 if ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) {
818 if ( filter( ['error', 'missing'], registry[m].dependencies ).length > 0 ) {
819 registry[m].state = 'error';
824 } while ( stateChange );
827 // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
828 for ( j = 0; j < jobs.length; j += 1 ) {
829 hasErrors = filter( ['error', 'missing'], jobs[j].dependencies ).length > 0;
830 if ( hasErrors || allReady( jobs[j].dependencies ) ) {
831 // All dependencies satisfied, or some have errors
837 throw new Error( 'Module ' + module + ' failed.');
839 if ( $.isFunction( job.ready ) ) {
844 if ( $.isFunction( job.error ) ) {
846 job.error( e, [module] );
848 // A user-defined operation raised an exception. Swallow to protect
849 // our state machine!
850 log( 'Exception thrown by job.error()', ex );
857 if ( registry[module].state === 'ready' ) {
858 // The current module became 'ready'. Recursively execute all dependent modules that are loaded
859 // and now have all dependencies satisfied.
860 for ( m in registry ) {
861 if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) {
869 * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
870 * depending on whether document-ready has occurred yet and whether we are in async mode.
873 * @param {string} src URL to script, will be used as the src attribute in the script tag
874 * @param {Function} [callback] Callback which will be run when the script is done
876 function addScript( src, callback, async ) {
877 /*jshint evil:true */
878 var script, head, done;
880 // Using isReady directly instead of storing it locally from
881 // a $.fn.ready callback (bug 31895).
882 if ( $.isReady || async ) {
883 // Can't use jQuery.getScript because that only uses <script> for cross-domain,
884 // it uses XHR and eval for same-domain scripts, which we don't want because it
885 // messes up line numbers.
886 // The below is based on jQuery ([jquery@1.8.2]/src/ajax/script.js)
888 // IE-safe way of getting the <head>. document.head isn't supported
889 // in old IE, and doesn't work when in the <head>.
891 head = document.getElementsByTagName( 'head' )[0] || document.body;
893 script = document.createElement( 'script' );
896 if ( $.isFunction( callback ) ) {
897 script.onload = script.onreadystatechange = function () {
902 || /loaded|complete/.test( script.readyState )
907 // Handle memory leak in IE
908 script.onload = script.onreadystatechange = null;
910 // Detach the element from the document
911 if ( script.parentNode ) {
912 script.parentNode.removeChild( script );
915 // Dereference the element from javascript
923 if ( window.opera ) {
924 // Appending to the <head> blocks rendering completely in Opera,
925 // so append to the <body> after document ready. This means the
926 // scripts only start loading after the document has been rendered,
927 // but so be it. Opera users don't deserve faster web pages if their
928 // browser makes it impossible.
930 document.body.appendChild( script );
933 head.appendChild( script );
936 document.write( mw.html.element( 'script', { 'src': src }, '' ) );
937 if ( $.isFunction( callback ) ) {
938 // Document.write is synchronous, so this is called when it's done
939 // FIXME: that's a lie. doc.write isn't actually synchronous
946 * Executes a loaded module, making it ready to use
949 * @param {string} module Module name to execute
951 function execute( module ) {
952 var key, value, media, i, urls, cssHandle, checkCssHandles,
953 cssHandlesRegistered = false;
955 if ( registry[module] === undefined ) {
956 throw new Error( 'Module has not been registered yet: ' + module );
957 } else if ( registry[module].state === 'registered' ) {
958 throw new Error( 'Module has not been requested from the server yet: ' + module );
959 } else if ( registry[module].state === 'loading' ) {
960 throw new Error( 'Module has not completed loading yet: ' + module );
961 } else if ( registry[module].state === 'ready' ) {
962 throw new Error( 'Module has already been executed: ' + module );
966 * Define loop-function here for efficiency
967 * and to avoid re-using badly scoped variables.
970 function addLink( media, url ) {
971 var el = document.createElement( 'link' );
972 getMarker().before( el ); // IE: Insert in dom before setting href
973 el.rel = 'stylesheet';
974 if ( media && media !== 'all' ) {
980 function runScript() {
981 var script, markModuleReady, nestedAddScript;
983 script = registry[module].script;
984 markModuleReady = function () {
985 registry[module].state = 'ready';
986 handlePending( module );
988 nestedAddScript = function ( arr, callback, async, i ) {
989 // Recursively call addScript() in its own callback
990 // for each element of arr.
991 if ( i >= arr.length ) {
992 // We're at the end of the array
997 addScript( arr[i], function () {
998 nestedAddScript( arr, callback, async, i + 1 );
1002 if ( $.isArray( script ) ) {
1003 nestedAddScript( script, markModuleReady, registry[module].async, 0 );
1004 } else if ( $.isFunction( script ) ) {
1005 registry[module].state = 'ready';
1007 handlePending( module );
1010 // This needs to NOT use mw.log because these errors are common in production mode
1011 // and not in debug mode, such as when a symbol that should be global isn't exported
1012 log( 'Exception thrown by ' + module + ': ' + e.message, e );
1013 registry[module].state = 'error';
1014 handlePending( module );
1018 // This used to be inside runScript, but since that is now fired asychronously
1019 // (after CSS is loaded) we need to set it here right away. It is crucial that
1020 // when execute() is called this is set synchronously, otherwise modules will get
1021 // executed multiple times as the registry will state that it isn't loading yet.
1022 registry[module].state = 'loading';
1024 // Add localizations to message system
1025 if ( $.isPlainObject( registry[module].messages ) ) {
1026 mw.messages.set( registry[module].messages );
1029 // Make sure we don't run the scripts until all (potentially asynchronous)
1030 // stylesheet insertions have completed.
1033 checkCssHandles = function () {
1034 // cssHandlesRegistered ensures we don't take off too soon, e.g. when
1035 // one of the cssHandles is fired while we're still creating more handles.
1036 if ( cssHandlesRegistered && pending === 0 && runScript ) {
1038 runScript = undefined; // Revoke
1041 cssHandle = function () {
1042 var check = checkCssHandles;
1044 return function () {
1048 check = undefined; // Revoke
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 asynchrounous 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 * @inheritdoc #newStyleTag
1212 addStyleTag: newStyleTag,
1215 * Batch-request queued dependencies from the server.
1218 var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
1219 source, group, g, i, modules, maxVersion, sourceLoadScript,
1220 currReqBase, currReqBaseLength, moduleMap, l,
1221 lastDotIndex, prefix, suffix, bytesAdded, async;
1223 // Build a list of request parameters common to all requests.
1225 skin: mw.config.get( 'skin' ),
1226 lang: mw.config.get( 'wgUserLanguage' ),
1227 debug: mw.config.get( 'debug' )
1229 // Split module batch by source and by group.
1231 maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 );
1233 // Appends a list of modules from the queue to the batch
1234 for ( q = 0; q < queue.length; q += 1 ) {
1235 // Only request modules which are registered
1236 if ( registry[queue[q]] !== undefined && registry[queue[q]].state === 'registered' ) {
1237 // Prevent duplicate entries
1238 if ( $.inArray( queue[q], batch ) === -1 ) {
1239 batch[batch.length] = queue[q];
1240 // Mark registered modules as loading
1241 registry[queue[q]].state = 'loading';
1245 // Early exit if there's nothing to load...
1246 if ( !batch.length ) {
1250 // The queue has been processed into the batch, clear up the queue.
1253 // Always order modules alphabetically to help reduce cache
1254 // misses for otherwise identical content.
1257 // Split batch by source and by group.
1258 for ( b = 0; b < batch.length; b += 1 ) {
1259 bSource = registry[batch[b]].source;
1260 bGroup = registry[batch[b]].group;
1261 if ( splits[bSource] === undefined ) {
1262 splits[bSource] = {};
1264 if ( splits[bSource][bGroup] === undefined ) {
1265 splits[bSource][bGroup] = [];
1267 bSourceGroup = splits[bSource][bGroup];
1268 bSourceGroup[bSourceGroup.length] = batch[b];
1271 // Clear the batch - this MUST happen before we append any
1272 // script elements to the body or it's possible that a script
1273 // will be locally cached, instantly load, and work the batch
1274 // again, all before we've cleared it causing each request to
1275 // include modules which are already loaded.
1278 for ( source in splits ) {
1280 sourceLoadScript = sources[source].loadScript;
1282 for ( group in splits[source] ) {
1284 // Cache access to currently selected list of
1285 // modules for this group from this source.
1286 modules = splits[source][group];
1288 // Calculate the highest timestamp
1290 for ( g = 0; g < modules.length; g += 1 ) {
1291 if ( registry[modules[g]].version > maxVersion ) {
1292 maxVersion = registry[modules[g]].version;
1296 currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase );
1297 // For user modules append a user name to the request.
1298 if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
1299 currReqBase.user = mw.config.get( 'wgUserName' );
1301 currReqBaseLength = $.param( currReqBase ).length;
1303 // We may need to split up the request to honor the query string length limit,
1304 // so build it piece by piece.
1305 l = currReqBaseLength + 9; // '&modules='.length == 9
1307 moduleMap = {}; // { prefix: [ suffixes ] }
1309 for ( i = 0; i < modules.length; i += 1 ) {
1310 // Determine how many bytes this module would add to the query string
1311 lastDotIndex = modules[i].lastIndexOf( '.' );
1312 // Note that these substr() calls work even if lastDotIndex == -1
1313 prefix = modules[i].substr( 0, lastDotIndex );
1314 suffix = modules[i].substr( lastDotIndex + 1 );
1315 bytesAdded = moduleMap[prefix] !== undefined
1316 ? suffix.length + 3 // '%2C'.length == 3
1317 : modules[i].length + 3; // '%7C'.length == 3
1319 // If the request would become too long, create a new one,
1320 // but don't create empty requests
1321 if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
1322 // This request would become too long, create a new one
1323 // and fire off the old one
1324 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1327 l = currReqBaseLength + 9;
1329 if ( moduleMap[prefix] === undefined ) {
1330 moduleMap[prefix] = [];
1332 moduleMap[prefix].push( suffix );
1333 if ( !registry[modules[i]].async ) {
1334 // If this module is blocking, make the entire request blocking
1335 // This is slightly suboptimal, but in practice mixing of blocking
1336 // and async modules will only occur in debug mode.
1341 // If there's anything left in moduleMap, request that too
1342 if ( !$.isEmptyObject( moduleMap ) ) {
1343 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1350 * Register a source.
1352 * @param {string} id Short lowercase a-Z string representing a source, only used internally.
1353 * @param {Object} props Object containing only the loadScript property which is a url to
1354 * the load.php location of the source.
1357 addSource: function ( id, props ) {
1359 // Allow multiple additions
1360 if ( typeof id === 'object' ) {
1361 for ( source in id ) {
1362 mw.loader.addSource( source, id[source] );
1367 if ( sources[id] !== undefined ) {
1368 throw new Error( 'source already registered: ' + id );
1371 sources[id] = props;
1377 * Register a module, letting the system know about it and its
1378 * properties. Startup modules contain calls to this function.
1380 * @param {string} module Module name
1381 * @param {number} version Module version number as a timestamp (falls backs to 0)
1382 * @param {string|Array|Function} dependencies One string or array of strings of module
1383 * names on which this module depends, or a function that returns that array.
1384 * @param {string} [group=null] Group which the module is in
1385 * @param {string} [source='local'] Name of the source
1387 register: function ( module, version, dependencies, group, source ) {
1389 // Allow multiple registration
1390 if ( typeof module === 'object' ) {
1391 for ( m = 0; m < module.length; m += 1 ) {
1392 // module is an array of module names
1393 if ( typeof module[m] === 'string' ) {
1394 mw.loader.register( module[m] );
1395 // module is an array of arrays
1396 } else if ( typeof module[m] === 'object' ) {
1397 mw.loader.register.apply( mw.loader, module[m] );
1403 if ( typeof module !== 'string' ) {
1404 throw new Error( 'module must be a string, not a ' + typeof module );
1406 if ( registry[module] !== undefined ) {
1407 throw new Error( 'module already registered: ' + module );
1409 // List the module as registered
1410 registry[module] = {
1411 version: version !== undefined ? parseInt( version, 10 ) : 0,
1413 group: typeof group === 'string' ? group : null,
1414 source: typeof source === 'string' ? source: 'local',
1417 if ( typeof dependencies === 'string' ) {
1418 // Allow dependencies to be given as a single module name
1419 registry[module].dependencies = [ dependencies ];
1420 } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
1421 // Allow dependencies to be given as an array of module names
1422 // or a function which returns an array
1423 registry[module].dependencies = dependencies;
1428 * Implement a module given the components that make up the module.
1430 * When #load or #using requests one or more modules, the server
1431 * response contain calls to this function.
1433 * All arguments are required.
1435 * @param {string} module Name of module
1436 * @param {Function|Array} script Function with module code or Array of URLs to
1437 * be used as the src attribute of a new `<script>` tag.
1438 * @param {Object} style Should follow one of the following patterns:
1439 * { "css": [css, ..] }
1440 * { "url": { <media>: [url, ..] } }
1441 * And for backwards compatibility (needs to be supported forever due to caching):
1443 * { <media>: [url, ..] }
1445 * The reason css strings are not concatenated anymore is bug 31676. We now check
1446 * whether it's safe to extend the stylesheet (see #canExpandStylesheetWith).
1448 * @param {Object} msgs List of key/value pairs to be added to {@link mw#messages}.
1450 implement: function ( module, script, style, msgs ) {
1452 if ( typeof module !== 'string' ) {
1453 throw new Error( 'module must be a string, not a ' + typeof module );
1455 if ( !$.isFunction( script ) && !$.isArray( script ) ) {
1456 throw new Error( 'script must be a function or an array, not a ' + typeof script );
1458 if ( !$.isPlainObject( style ) ) {
1459 throw new Error( 'style must be an object, not a ' + typeof style );
1461 if ( !$.isPlainObject( msgs ) ) {
1462 throw new Error( 'msgs must be an object, not a ' + typeof msgs );
1464 // Automatically register module
1465 if ( registry[module] === undefined ) {
1466 mw.loader.register( module );
1468 // Check for duplicate implementation
1469 if ( registry[module] !== undefined && registry[module].script !== undefined ) {
1470 throw new Error( 'module already implemented: ' + module );
1472 // Attach components
1473 registry[module].script = script;
1474 registry[module].style = style;
1475 registry[module].messages = msgs;
1476 // The module may already have been marked as erroneous
1477 if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
1478 registry[module].state = 'loaded';
1479 if ( allReady( registry[module].dependencies ) ) {
1486 * Execute a function as soon as one or more required modules are ready.
1488 * @param {string|Array} dependencies Module name or array of modules names the callback
1489 * dependends on to be ready before executing
1490 * @param {Function} [ready] callback to execute when all dependencies are ready
1491 * @param {Function} [error] callback to execute when if dependencies have a errors
1493 using: function ( dependencies, ready, error ) {
1494 var tod = typeof dependencies;
1496 if ( tod !== 'object' && tod !== 'string' ) {
1497 throw new Error( 'dependencies must be a string or an array, not a ' + tod );
1499 // Allow calling with a single dependency as a string
1500 if ( tod === 'string' ) {
1501 dependencies = [ dependencies ];
1503 // Resolve entire dependency map
1504 dependencies = resolve( dependencies );
1505 if ( allReady( dependencies ) ) {
1506 // Run ready immediately
1507 if ( $.isFunction( ready ) ) {
1510 } else if ( filter( ['error', 'missing'], dependencies ).length ) {
1511 // Execute error immediately if any dependencies have errors
1512 if ( $.isFunction( error ) ) {
1513 error( new Error( 'one or more dependencies have state "error" or "missing"' ),
1517 // Not all dependencies are ready: queue up a request
1518 request( dependencies, ready, error );
1523 * Load an external script or one or more modules.
1525 * @param {string|Array} modules Either the name of a module, array of modules,
1526 * or a URL of an external script or style
1527 * @param {string} [type='text/javascript'] mime-type to use if calling with a URL of an
1528 * external script or style; acceptable values are "text/css" and
1529 * "text/javascript"; if no type is provided, text/javascript is assumed.
1530 * @param {boolean} [async] If true, load modules asynchronously
1531 * even if document ready has not yet occurred. If false, block before
1532 * document ready and load async after. If not set, true will be
1533 * assumed if loading a URL, and false will be assumed otherwise.
1535 load: function ( modules, type, async ) {
1536 var filtered, m, module, l;
1539 if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
1540 throw new Error( 'modules must be a string or an array, not a ' + typeof modules );
1542 // Allow calling with an external url or single dependency as a string
1543 if ( typeof modules === 'string' ) {
1544 // Support adding arbitrary external scripts
1545 if ( /^(https?:)?\/\//.test( modules ) ) {
1546 if ( async === undefined ) {
1547 // Assume async for bug 34542
1550 if ( type === 'text/css' ) {
1551 // IE7-8 throws security warnings when inserting a <link> tag
1552 // with a protocol-relative URL set though attributes (instead of
1553 // properties) - when on HTTPS. See also bug #.
1554 l = document.createElement( 'link' );
1555 l.rel = 'stylesheet';
1557 $( 'head' ).append( l );
1560 if ( type === 'text/javascript' || type === undefined ) {
1561 addScript( modules, null, async );
1565 throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
1567 // Called with single module
1568 modules = [ modules ];
1571 // Filter out undefined modules, otherwise resolve() will throw
1572 // an exception for trying to load an undefined module.
1573 // Undefined modules are acceptable here in load(), because load() takes
1574 // an array of unrelated modules, whereas the modules passed to
1575 // using() are related and must all be loaded.
1576 for ( filtered = [], m = 0; m < modules.length; m += 1 ) {
1577 module = registry[modules[m]];
1578 if ( module !== undefined ) {
1579 if ( $.inArray( module.state, ['error', 'missing'] ) === -1 ) {
1580 filtered[filtered.length] = modules[m];
1585 if ( filtered.length === 0 ) {
1588 // Resolve entire dependency map
1589 filtered = resolve( filtered );
1590 // If all modules are ready, nothing to be done
1591 if ( allReady( filtered ) ) {
1594 // If any modules have errors: also quit.
1595 if ( filter( ['error', 'missing'], filtered ).length ) {
1598 // Since some modules are not yet ready, queue up a request.
1599 request( filtered, undefined, undefined, async );
1603 * Change the state of one or more modules.
1605 * @param {string|Object} module module name or object of module name/state pairs
1606 * @param {string} state state name
1608 state: function ( module, state ) {
1611 if ( typeof module === 'object' ) {
1612 for ( m in module ) {
1613 mw.loader.state( m, module[m] );
1617 if ( registry[module] === undefined ) {
1618 mw.loader.register( module );
1620 if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1
1621 && registry[module].state !== state ) {
1622 // Make sure pending modules depending on this one get executed if their
1623 // dependencies are now fulfilled!
1624 registry[module].state = state;
1625 handlePending( module );
1627 registry[module].state = state;
1632 * Get the version of a module.
1634 * @param {string} module Name of module to get version for
1636 getVersion: function ( module ) {
1637 if ( registry[module] !== undefined && registry[module].version !== undefined ) {
1638 return formatVersionNumber( registry[module].version );
1644 * @inheritdoc #getVersion
1645 * @deprecated since 1.18 use #getVersion instead
1647 version: function () {
1648 return mw.loader.getVersion.apply( mw.loader, arguments );
1652 * Get the state of a module.
1654 * @param {string} module name of module to get state for
1656 getState: function ( module ) {
1657 if ( registry[module] !== undefined && registry[module].state !== undefined ) {
1658 return registry[module].state;
1664 * Get names of all registered modules.
1668 getModuleNames: function () {
1669 return $.map( registry, function ( i, key ) {
1675 * Load the `mediawiki.user` module.
1677 * For backwards-compatibility with cached pages from before 2013 where:
1679 * - the `mediawiki.user` module didn't exist yet
1680 * - `mw.user` was still part of mediawiki.js
1681 * - `mw.loader.go` still existed and called after `mw.loader.load()`
1684 mw.loader.load( 'mediawiki.user' );
1690 * HTML construction helper functions
1697 * output = Html.element( 'div', {}, new Html.Raw(
1698 * Html.element( 'img', { src: '<' } )
1700 * mw.log( output ); // <div><img src="<"/></div>
1705 html: ( function () {
1706 function escapeCallback( s ) {
1723 * Escape a string for HTML. Converts special characters to HTML entities.
1724 * @param {string} s The string to escape
1726 escape: function ( s ) {
1727 return s.replace( /['"<>&]/g, escapeCallback );
1731 * Create an HTML element string, with safe escaping.
1733 * @param {string} name The tag name.
1734 * @param {Object} attrs An object with members mapping element names to values
1735 * @param {Mixed} contents The contents of the element. May be either:
1736 * - string: The string is escaped.
1737 * - null or undefined: The short closing form is used, e.g. <br/>.
1738 * - this.Raw: The value attribute is included without escaping.
1739 * - this.Cdata: The value attribute is included, and an exception is
1740 * thrown if it contains an illegal ETAGO delimiter.
1741 * See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2
1743 element: function ( name, attrs, contents ) {
1744 var v, attrName, s = '<' + name;
1746 for ( attrName in attrs ) {
1747 v = attrs[attrName];
1748 // Convert name=true, to name=name
1752 } else if ( v === false ) {
1755 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
1757 if ( contents === undefined || contents === null ) {
1764 switch ( typeof contents ) {
1767 s += this.escape( contents );
1771 // Convert to string
1772 s += String( contents );
1775 if ( contents instanceof this.Raw ) {
1776 // Raw HTML inclusion
1777 s += contents.value;
1778 } else if ( contents instanceof this.Cdata ) {
1780 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
1781 throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
1783 s += contents.value;
1785 throw new Error( 'mw.html.element: Invalid type of contents' );
1788 s += '</' + name + '>';
1793 * Wrapper object for raw HTML passed to mw.html.element().
1794 * @class mw.html.Raw
1796 Raw: function ( value ) {
1801 * Wrapper object for CDATA element contents passed to mw.html.element()
1802 * @class mw.html.Cdata
1804 Cdata: function ( value ) {
1810 // Skeleton user object. mediawiki.user.js extends this
1817 * Registry and firing of events.
1819 * MediaWiki has various interface components that are extended, enhanced
1820 * or manipulated in some other way by extensions, gadgets and even
1823 * This framework helps streamlining the timing of when these other
1824 * code paths fire their plugins (instead of using document-ready,
1825 * which can and should be limited to firing only once).
1827 * Features like navigating to other wiki pages, previewing an edit
1828 * and editing itself – without a refresh – can then retrigger these
1829 * hooks accordingly to ensure everything still works as expected.
1833 * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
1834 * mw.hook( 'wikipage.content' ).fire( $content );
1836 * Handlers can be added and fired for arbitrary event names at any time. The same
1837 * event can be fired multiple times. The last run of an event is memorized
1838 * (similar to `$(document).ready` and `$.Deferred().done`).
1839 * This means if an event is fired, and a handler added afterwards, the added
1840 * function will be fired right away with the last given event data.
1842 * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
1843 * Thus allowing flexible use and optimal maintainability and authority control.
1844 * You can pass around the `add` and/or `fire` method to another piece of code
1845 * without it having to know the event name (or `mw.hook` for that matter).
1847 * var h = mw.hook( 'bar.ready' );
1848 * new mw.Foo( .. ).fetch( { callback: h.fire } );
1850 * Note: Events are documented with an underscore instead of a dot in the event
1851 * name due to jsduck not supporting dots in that position.
1855 hook: ( function () {
1859 * Create an instance of mw.hook.
1863 * @param {string} name Name of hook.
1866 return function ( name ) {
1867 var list = lists[name] || ( lists[name] = $.Callbacks( 'memory' ) );
1871 * Register a hook handler
1872 * @param {Function...} handler Function to bind.
1878 * Unregister a hook handler
1879 * @param {Function...} handler Function to unbind.
1882 remove: list.remove,
1886 * @param {Mixed...} data
1890 return list.fireWith( null, slice.call( arguments ) );
1899 // Alias $j to jQuery for backwards compatibility
1902 // Attach to window and globally alias
1903 window.mw = window.mediaWiki = mw;
1905 // Auto-register from pre-loaded startup scripts
1906 if ( jQuery.isFunction( window.startUp ) ) {
1908 window.startUp = undefined;