Merge "Apply coding conventions for JavaScript"
[lhc/web/wiklou.git] / resources / src / mediawiki / mediawiki.js
1 /**
2 * Base library for MediaWiki.
3 *
4 * Exposed globally as `mediaWiki` with `mw` as shortcut.
5 *
6 * @class mw
7 * @alternateClassName mediaWiki
8 * @singleton
9 */
10 ( function ( $ ) {
11 'use strict';
12
13 /* Private Members */
14
15 var mw,
16 hasOwn = Object.prototype.hasOwnProperty,
17 slice = Array.prototype.slice,
18 trackCallbacks = $.Callbacks( 'memory' ),
19 trackQueue = [];
20
21 /**
22 * Log a message to window.console, if possible. Useful to force logging of some
23 * errors that are otherwise hard to detect (I.e., this logs also in production mode).
24 * Gets console references in each invocation, so that delayed debugging tools work
25 * fine. No need for optimization here, which would only result in losing logs.
26 *
27 * @private
28 * @method log_
29 * @param {string} msg text for the log entry.
30 * @param {Error} [e]
31 */
32 function log( msg, e ) {
33 var console = window.console;
34 if ( console && console.log ) {
35 console.log( msg );
36 // If we have an exception object, log it through .error() to trigger
37 // proper stacktraces in browsers that support it. There are no (known)
38 // browsers that don't support .error(), that do support .log() and
39 // have useful exception handling through .log().
40 if ( e && console.error ) {
41 console.error( String( e ), e );
42 }
43 }
44 }
45
46 /* Object constructors */
47
48 /**
49 * Creates an object that can be read from or written to from prototype functions
50 * that allow both single and multiple variables at once.
51 *
52 * @example
53 *
54 * var addies, wanted, results;
55 *
56 * // Create your address book
57 * addies = new mw.Map();
58 *
59 * // This data could be coming from an external source (eg. API/AJAX)
60 * addies.set( {
61 * 'John Doe' : '10 Wall Street, New York, USA',
62 * 'Jane Jackson' : '21 Oxford St, London, UK',
63 * 'Dominique van Halen' : 'Kalverstraat 7, Amsterdam, NL'
64 * } );
65 *
66 * wanted = ['Dominique van Halen', 'George Johnson', 'Jane Jackson'];
67 *
68 * // You can detect missing keys first
69 * if ( !addies.exists( wanted ) ) {
70 * // One or more are missing (in this case: "George Johnson")
71 * mw.log( 'One or more names were not found in your address book' );
72 * }
73 *
74 * // Or just let it give you what it can
75 * results = addies.get( wanted, 'Middle of Nowhere, Alaska, US' );
76 * mw.log( results['Jane Jackson'] ); // "21 Oxford St, London, UK"
77 * mw.log( results['George Johnson'] ); // "Middle of Nowhere, Alaska, US"
78 *
79 * @class mw.Map
80 *
81 * @constructor
82 * @param {Object|boolean} [values] Value-bearing object to map, or boolean
83 * true to map over the global object. Defaults to an empty object.
84 */
85 function Map( values ) {
86 this.values = values === true ? window : ( values || {} );
87 return this;
88 }
89
90 Map.prototype = {
91 /**
92 * Get the value of one or multiple keys.
93 *
94 * If called with no arguments, all values will be returned.
95 *
96 * @param {string|Array} selection String key or array of keys to get values for.
97 * @param {Mixed} [fallback] Value to use in case key(s) do not exist.
98 * @return mixed If selection was a string returns the value or null,
99 * If selection was an array, returns an object of key/values (value is null if not found),
100 * If selection was not passed or invalid, will return the 'values' object member (be careful as
101 * objects are always passed by reference in JavaScript!).
102 * @return {string|Object|null} Values as a string or object, null if invalid/inexistent.
103 */
104 get: function ( selection, fallback ) {
105 var results, i;
106 // If we only do this in the `return` block, it'll fail for the
107 // call to get() from the mutli-selection block.
108 fallback = arguments.length > 1 ? fallback : null;
109
110 if ( $.isArray( selection ) ) {
111 selection = slice.call( selection );
112 results = {};
113 for ( i = 0; i < selection.length; i++ ) {
114 results[selection[i]] = this.get( selection[i], fallback );
115 }
116 return results;
117 }
118
119 if ( typeof selection === 'string' ) {
120 if ( !hasOwn.call( this.values, selection ) ) {
121 return fallback;
122 }
123 return this.values[selection];
124 }
125
126 if ( selection === undefined ) {
127 return this.values;
128 }
129
130 // invalid selection key
131 return null;
132 },
133
134 /**
135 * Sets one or multiple key/value pairs.
136 *
137 * @param {string|Object} selection String key to set value for, or object mapping keys to values.
138 * @param {Mixed} [value] Value to set (optional, only in use when key is a string)
139 * @return {Boolean} This returns true on success, false on failure.
140 */
141 set: function ( selection, value ) {
142 var s;
143
144 if ( $.isPlainObject( selection ) ) {
145 for ( s in selection ) {
146 this.values[s] = selection[s];
147 }
148 return true;
149 }
150 if ( typeof selection === 'string' && arguments.length > 1 ) {
151 this.values[selection] = value;
152 return true;
153 }
154 return false;
155 },
156
157 /**
158 * Checks if one or multiple keys exist.
159 *
160 * @param {Mixed} selection String key or array of keys to check
161 * @return {boolean} Existence of key(s)
162 */
163 exists: function ( selection ) {
164 var s;
165
166 if ( $.isArray( selection ) ) {
167 for ( s = 0; s < selection.length; s++ ) {
168 if ( typeof selection[s] !== 'string' || !hasOwn.call( this.values, selection[s] ) ) {
169 return false;
170 }
171 }
172 return true;
173 }
174 return typeof selection === 'string' && hasOwn.call( this.values, selection );
175 }
176 };
177
178 /**
179 * Object constructor for messages.
180 *
181 * Similar to the Message class in MediaWiki PHP.
182 *
183 * Format defaults to 'text'.
184 *
185 * @example
186 *
187 * var obj, str;
188 * mw.messages.set( {
189 * 'hello': 'Hello world',
190 * 'hello-user': 'Hello, $1!',
191 * 'welcome-user': 'Welcome back to $2, $1! Last visit by $1: $3'
192 * } );
193 *
194 * obj = new mw.Message( mw.messages, 'hello' );
195 * mw.log( obj.text() );
196 * // Hello world
197 *
198 * obj = new mw.Message( mw.messages, 'hello-user', [ 'John Doe' ] );
199 * mw.log( obj.text() );
200 * // Hello, John Doe!
201 *
202 * obj = new mw.Message( mw.messages, 'welcome-user', [ 'John Doe', 'Wikipedia', '2 hours ago' ] );
203 * mw.log( obj.text() );
204 * // Welcome back to Wikipedia, John Doe! Last visit by John Doe: 2 hours ago
205 *
206 * // Using mw.message shortcut
207 * obj = mw.message( 'hello-user', 'John Doe' );
208 * mw.log( obj.text() );
209 * // Hello, John Doe!
210 *
211 * // Using mw.msg shortcut
212 * str = mw.msg( 'hello-user', 'John Doe' );
213 * mw.log( str );
214 * // Hello, John Doe!
215 *
216 * // Different formats
217 * obj = new mw.Message( mw.messages, 'hello-user', [ 'John "Wiki" <3 Doe' ] );
218 *
219 * obj.format = 'text';
220 * str = obj.toString();
221 * // Same as:
222 * str = obj.text();
223 *
224 * mw.log( str );
225 * // Hello, John "Wiki" <3 Doe!
226 *
227 * mw.log( obj.escaped() );
228 * // Hello, John &quot;Wiki&quot; &lt;3 Doe!
229 *
230 * @class mw.Message
231 *
232 * @constructor
233 * @param {mw.Map} map Message storage
234 * @param {string} key
235 * @param {Array} [parameters]
236 */
237 function Message( map, key, parameters ) {
238 this.format = 'text';
239 this.map = map;
240 this.key = key;
241 this.parameters = parameters === undefined ? [] : slice.call( parameters );
242 return this;
243 }
244
245 Message.prototype = {
246 /**
247 * Simple message parser, does $N replacement and nothing else.
248 *
249 * This may be overridden to provide a more complex message parser.
250 *
251 * The primary override is in mediawiki.jqueryMsg.
252 *
253 * This function will not be called for nonexistent messages.
254 */
255 parser: function () {
256 var parameters = this.parameters;
257 return this.map.get( this.key ).replace( /\$(\d+)/g, function ( str, match ) {
258 var index = parseInt( match, 10 ) - 1;
259 return parameters[index] !== undefined ? parameters[index] : '$' + match;
260 } );
261 },
262
263 /**
264 * Appends (does not replace) parameters for replacement to the .parameters property.
265 *
266 * @param {Array} parameters
267 * @chainable
268 */
269 params: function ( parameters ) {
270 var i;
271 for ( i = 0; i < parameters.length; i += 1 ) {
272 this.parameters.push( parameters[i] );
273 }
274 return this;
275 },
276
277 /**
278 * Converts message object to its string form based on the state of format.
279 *
280 * @return {string} Message as a string in the current form or `<key>` if key does not exist.
281 */
282 toString: function () {
283 var text;
284
285 if ( !this.exists() ) {
286 // Use <key> as text if key does not exist
287 if ( this.format === 'escaped' || this.format === 'parse' ) {
288 // format 'escaped' and 'parse' need to have the brackets and key html escaped
289 return mw.html.escape( '<' + this.key + '>' );
290 }
291 return '<' + this.key + '>';
292 }
293
294 if ( this.format === 'plain' || this.format === 'text' || this.format === 'parse' ) {
295 text = this.parser();
296 }
297
298 if ( this.format === 'escaped' ) {
299 text = this.parser();
300 text = mw.html.escape( text );
301 }
302
303 return text;
304 },
305
306 /**
307 * Changes format to 'parse' and converts message to string
308 *
309 * If jqueryMsg is loaded, this parses the message text from wikitext
310 * (where supported) to HTML
311 *
312 * Otherwise, it is equivalent to plain.
313 *
314 * @return {string} String form of parsed message
315 */
316 parse: function () {
317 this.format = 'parse';
318 return this.toString();
319 },
320
321 /**
322 * Changes format to 'plain' and converts message to string
323 *
324 * This substitutes parameters, but otherwise does not change the
325 * message text.
326 *
327 * @return {string} String form of plain message
328 */
329 plain: function () {
330 this.format = 'plain';
331 return this.toString();
332 },
333
334 /**
335 * Changes format to 'text' and converts message to string
336 *
337 * If jqueryMsg is loaded, {{-transformation is done where supported
338 * (such as {{plural:}}, {{gender:}}, {{int:}}).
339 *
340 * Otherwise, it is equivalent to plain.
341 */
342 text: function () {
343 this.format = 'text';
344 return this.toString();
345 },
346
347 /**
348 * Changes the format to 'escaped' and converts message to string
349 *
350 * This is equivalent to using the 'text' format (see text method), then
351 * HTML-escaping the output.
352 *
353 * @return {string} String form of html escaped message
354 */
355 escaped: function () {
356 this.format = 'escaped';
357 return this.toString();
358 },
359
360 /**
361 * Checks if message exists
362 *
363 * @see mw.Map#exists
364 * @return {boolean}
365 */
366 exists: function () {
367 return this.map.exists( this.key );
368 }
369 };
370
371 /**
372 * @class mw
373 */
374 mw = {
375 /* Public Members */
376
377 /**
378 * Get the current time, measured in milliseconds since January 1, 1970 (UTC).
379 *
380 * On browsers that implement the Navigation Timing API, this function will produce floating-point
381 * values with microsecond precision that are guaranteed to be monotonic. On all other browsers,
382 * it will fall back to using `Date`.
383 *
384 * @return {number} Current time
385 */
386 now: ( function () {
387 var perf = window.performance,
388 navStart = perf && perf.timing && perf.timing.navigationStart;
389 return navStart && typeof perf.now === 'function' ?
390 function () { return navStart + perf.now(); } :
391 function () { return +new Date(); };
392 }() ),
393
394 /**
395 * Track an analytic event.
396 *
397 * This method provides a generic means for MediaWiki JavaScript code to capture state
398 * information for analysis. Each logged event specifies a string topic name that describes
399 * the kind of event that it is. Topic names consist of dot-separated path components,
400 * arranged from most general to most specific. Each path component should have a clear and
401 * well-defined purpose.
402 *
403 * Data handlers are registered via `mw.trackSubscribe`, and receive the full set of
404 * events that match their subcription, including those that fired before the handler was
405 * bound.
406 *
407 * @param {string} topic Topic name
408 * @param {Object} [data] Data describing the event, encoded as an object
409 */
410 track: function ( topic, data ) {
411 trackQueue.push( { topic: topic, timeStamp: mw.now(), data: data } );
412 trackCallbacks.fire( trackQueue );
413 },
414
415 /**
416 * Register a handler for subset of analytic events, specified by topic
417 *
418 * Handlers will be called once for each tracked event, including any events that fired before the
419 * handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
420 * the exact time at which the event fired, a string 'topic' property naming the event, and a
421 * 'data' property which is an object of event-specific data. The event topic and event data are
422 * also passed to the callback as the first and second arguments, respectively.
423 *
424 * @param {string} topic Handle events whose name starts with this string prefix
425 * @param {Function} callback Handler to call for each matching tracked event
426 */
427 trackSubscribe: function ( topic, callback ) {
428 var seen = 0;
429
430 trackCallbacks.add( function ( trackQueue ) {
431 var event;
432 for ( ; seen < trackQueue.length; seen++ ) {
433 event = trackQueue[ seen ];
434 if ( event.topic.indexOf( topic ) === 0 ) {
435 callback.call( event, event.topic, event.data );
436 }
437 }
438 } );
439 },
440
441 // Make the Map constructor publicly available.
442 Map: Map,
443
444 // Make the Message constructor publicly available.
445 Message: Message,
446
447 /**
448 * Map of configuration values
449 *
450 * Check out [the complete list of configuration values](https://www.mediawiki.org/wiki/Manual:Interface/JavaScript#mw.config)
451 * on mediawiki.org.
452 *
453 * If `$wgLegacyJavaScriptGlobals` is true, this Map will add its values to the
454 * global `window` object.
455 *
456 * @property {mw.Map} config
457 */
458 // Dummy placeholder. Re-assigned in ResourceLoaderStartupModule to an instance of `mw.Map`.
459 config: null,
460
461 /**
462 * Empty object that plugins can be installed in.
463 * @property
464 */
465 libs: {},
466
467 /**
468 * Access container for deprecated functionality that can be moved from
469 * from their legacy location and attached to this object (e.g. a global
470 * function that is deprecated and as stop-gap can be exposed through here).
471 *
472 * This was reserved for future use but never ended up being used.
473 *
474 * @deprecated since 1.22 Let deprecated identifiers keep their original name
475 * and use mw.log#deprecate to create an access container for tracking.
476 * @property
477 */
478 legacy: {},
479
480 /**
481 * Localization system
482 * @property {mw.Map}
483 */
484 messages: new Map(),
485
486 /**
487 * Templates associated with a module
488 * @property {mw.Map}
489 */
490 templates: new Map(),
491
492 /* Public Methods */
493
494 /**
495 * Get a message object.
496 *
497 * Shorcut for `new mw.Message( mw.messages, key, parameters )`.
498 *
499 * @see mw.Message
500 * @param {string} key Key of message to get
501 * @param {Mixed...} parameters Parameters for the $N replacements in messages.
502 * @return {mw.Message}
503 */
504 message: function ( key ) {
505 // Variadic arguments
506 var parameters = slice.call( arguments, 1 );
507 return new Message( mw.messages, key, parameters );
508 },
509
510 /**
511 * Get a message string using the (default) 'text' format.
512 *
513 * Shortcut for `mw.message( key, parameters... ).text()`.
514 *
515 * @see mw.Message
516 * @param {string} key Key of message to get
517 * @param {Mixed...} parameters Parameters for the $N replacements in messages.
518 * @return {string}
519 */
520 msg: function () {
521 return mw.message.apply( mw.message, arguments ).toString();
522 },
523
524 /**
525 * Dummy placeholder for {@link mw.log}
526 * @method
527 */
528 log: ( function () {
529 // Also update the restoration of methods in mediawiki.log.js
530 // when adding or removing methods here.
531 var log = function () {};
532
533 /**
534 * @class mw.log
535 * @singleton
536 */
537
538 /**
539 * Write a message the console's warning channel.
540 * Also logs a stacktrace for easier debugging.
541 * Each action is silently ignored if the browser doesn't support it.
542 *
543 * @param {string...} msg Messages to output to console
544 */
545 log.warn = function () {
546 var console = window.console;
547 if ( console && console.warn && console.warn.apply ) {
548 console.warn.apply( console, arguments );
549 if ( console.trace ) {
550 console.trace();
551 }
552 }
553 };
554
555 /**
556 * Create a property in a host object that, when accessed, will produce
557 * a deprecation warning in the console with backtrace.
558 *
559 * @param {Object} obj Host object of deprecated property
560 * @param {string} key Name of property to create in `obj`
561 * @param {Mixed} val The value this property should return when accessed
562 * @param {string} [msg] Optional text to include in the deprecation message.
563 */
564 log.deprecate = !Object.defineProperty ? function ( obj, key, val ) {
565 obj[key] = val;
566 } : function ( obj, key, val, msg ) {
567 msg = 'Use of "' + key + '" is deprecated.' + ( msg ? ( ' ' + msg ) : '' );
568 try {
569 Object.defineProperty( obj, key, {
570 configurable: true,
571 enumerable: true,
572 get: function () {
573 mw.track( 'mw.deprecate', key );
574 mw.log.warn( msg );
575 return val;
576 },
577 set: function ( newVal ) {
578 mw.track( 'mw.deprecate', key );
579 mw.log.warn( msg );
580 val = newVal;
581 }
582 } );
583 } catch ( err ) {
584 // IE8 can throw on Object.defineProperty
585 obj[key] = val;
586 }
587 };
588
589 return log;
590 }() ),
591
592 /**
593 * Client-side module loader which integrates with the MediaWiki ResourceLoader
594 * @class mw.loader
595 * @singleton
596 */
597 loader: ( function () {
598
599 /* Private Members */
600
601 /**
602 * Mapping of registered modules
603 *
604 * The jquery module is pre-registered, because it must have already
605 * been provided for this object to have been built, and in debug mode
606 * jquery would have been provided through a unique loader request,
607 * making it impossible to hold back registration of jquery until after
608 * mediawiki.
609 *
610 * For exact details on support for script, style and messages, look at
611 * mw.loader.implement.
612 *
613 * Format:
614 * {
615 * 'moduleName': {
616 * // At registry
617 * 'version': ############## (unix timestamp),
618 * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
619 * 'group': 'somegroup', (or) null,
620 * 'source': 'local', 'someforeignwiki', (or) null
621 * 'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing'
622 * 'skip': 'return !!window.Example', (or) null
623 *
624 * // Added during implementation
625 * 'skipped': true,
626 * 'script': ...,
627 * 'style': ...,
628 * 'messages': { 'key': 'value' },
629 * }
630 * }
631 *
632 * @property
633 * @private
634 */
635 var registry = {},
636 //
637 // Mapping of sources, keyed by source-id, values are strings.
638 // Format:
639 // {
640 // 'sourceId': 'http://foo.bar/w/load.php'
641 // }
642 //
643 sources = {},
644 // List of modules which will be loaded as when ready
645 batch = [],
646 // List of modules to be loaded
647 queue = [],
648 // List of callback functions waiting for modules to be ready to be called
649 jobs = [],
650 // Selector cache for the marker element. Use getMarker() to get/use the marker!
651 $marker = null,
652 // Buffer for addEmbeddedCSS.
653 cssBuffer = '',
654 // Callbacks for addEmbeddedCSS.
655 cssCallbacks = $.Callbacks();
656
657 /* Private methods */
658
659 function getMarker() {
660 // Cached
661 if ( !$marker ) {
662 $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' );
663 if ( !$marker.length ) {
664 mw.log( 'No <meta name="ResourceLoaderDynamicStyles"> found, inserting dynamically' );
665 $marker = $( '<meta>' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' );
666 }
667 }
668 return $marker;
669 }
670
671 /**
672 * Create a new style tag and add it to the DOM.
673 *
674 * @private
675 * @param {string} text CSS text
676 * @param {HTMLElement|jQuery} [nextnode=document.head] The element where the style tag should be
677 * inserted before. Otherwise it will be appended to `<head>`.
678 * @return {HTMLElement} Reference to the created `<style>` element.
679 */
680 function newStyleTag( text, nextnode ) {
681 var s = document.createElement( 'style' );
682 // Insert into document before setting cssText (bug 33305)
683 if ( nextnode ) {
684 // Must be inserted with native insertBefore, not $.fn.before.
685 // When using jQuery to insert it, like $nextnode.before( s ),
686 // then IE6 will throw "Access is denied" when trying to append
687 // to .cssText later. Some kind of weird security measure.
688 // http://stackoverflow.com/q/12586482/319266
689 // Works: jsfiddle.net/zJzMy/1
690 // Fails: jsfiddle.net/uJTQz
691 // Works again: http://jsfiddle.net/Azr4w/ (diff: the next 3 lines)
692 if ( nextnode.jquery ) {
693 nextnode = nextnode.get( 0 );
694 }
695 nextnode.parentNode.insertBefore( s, nextnode );
696 } else {
697 document.getElementsByTagName( 'head' )[0].appendChild( s );
698 }
699 if ( s.styleSheet ) {
700 // IE
701 s.styleSheet.cssText = text;
702 } else {
703 // Other browsers.
704 // (Safari sometimes borks on non-string values,
705 // play safe by casting to a string, just in case.)
706 s.appendChild( document.createTextNode( String( text ) ) );
707 }
708 return s;
709 }
710
711 /**
712 * Checks whether it is safe to add this css to a stylesheet.
713 *
714 * @private
715 * @param {string} cssText
716 * @return {boolean} False if a new one must be created.
717 */
718 function canExpandStylesheetWith( cssText ) {
719 // Makes sure that cssText containing `@import`
720 // rules will end up in a new stylesheet (as those only work when
721 // placed at the start of a stylesheet; bug 35562).
722 return cssText.indexOf( '@import' ) === -1;
723 }
724
725 /**
726 * Add a bit of CSS text to the current browser page.
727 *
728 * The CSS will be appended to an existing ResourceLoader-created `<style>` tag
729 * or create a new one based on whether the given `cssText` is safe for extension.
730 *
731 * @param {string} [cssText=cssBuffer] If called without cssText,
732 * the internal buffer will be inserted instead.
733 * @param {Function} [callback]
734 */
735 function addEmbeddedCSS( cssText, callback ) {
736 var $style, styleEl;
737
738 if ( callback ) {
739 cssCallbacks.add( callback );
740 }
741
742 // Yield once before inserting the <style> tag. There are likely
743 // more calls coming up which we can combine this way.
744 // Appending a stylesheet and waiting for the browser to repaint
745 // is fairly expensive, this reduces it (bug 45810)
746 if ( cssText ) {
747 // Be careful not to extend the buffer with css that needs a new stylesheet
748 if ( !cssBuffer || canExpandStylesheetWith( cssText ) ) {
749 // Linebreak for somewhat distinguishable sections
750 // (the rl-cachekey comment separating each)
751 cssBuffer += '\n' + cssText;
752 // TODO: Use requestAnimationFrame in the future which will
753 // perform even better by not injecting styles while the browser
754 // is paiting.
755 setTimeout( function () {
756 // Can't pass addEmbeddedCSS to setTimeout directly because Firefox
757 // (below version 13) has the non-standard behaviour of passing a
758 // numerical "lateness" value as first argument to this callback
759 // http://benalman.com/news/2009/07/the-mysterious-firefox-settime/
760 addEmbeddedCSS();
761 } );
762 return;
763 }
764
765 // This is a delayed call and we got a buffer still
766 } else if ( cssBuffer ) {
767 cssText = cssBuffer;
768 cssBuffer = '';
769 } else {
770 // This is a delayed call, but buffer is already cleared by
771 // another delayed call.
772 return;
773 }
774
775 // By default, always create a new <style>. Appending text to a <style>
776 // tag is bad as it means the contents have to be re-parsed (bug 45810).
777 //
778 // Except, of course, in IE 9 and below. In there we default to re-using and
779 // appending to a <style> tag due to the IE stylesheet limit (bug 31676).
780 if ( 'documentMode' in document && document.documentMode <= 9 ) {
781
782 $style = getMarker().prev();
783 // Verify that the the element before Marker actually is a
784 // <style> tag and one that came from ResourceLoader
785 // (not some other style tag or even a `<meta>` or `<script>`).
786 if ( $style.data( 'ResourceLoaderDynamicStyleTag' ) === true ) {
787 // There's already a dynamic <style> tag present and
788 // canExpandStylesheetWith() gave a green light to append more to it.
789 styleEl = $style.get( 0 );
790 if ( styleEl.styleSheet ) {
791 try {
792 styleEl.styleSheet.cssText += cssText; // IE
793 } catch ( e ) {
794 log( 'Stylesheet error', e );
795 }
796 } else {
797 styleEl.appendChild( document.createTextNode( String( cssText ) ) );
798 }
799 cssCallbacks.fire().empty();
800 return;
801 }
802 }
803
804 $( newStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true );
805
806 cssCallbacks.fire().empty();
807 }
808
809 /**
810 * Convert UNIX timestamp to ISO8601 format
811 * @param {number} timestamp UNIX timestamp
812 * @private
813 */
814 function formatVersionNumber( timestamp ) {
815 var d = new Date();
816 function pad( a, b, c ) {
817 return [a < 10 ? '0' + a : a, b < 10 ? '0' + b : b, c < 10 ? '0' + c : c].join( '' );
818 }
819 d.setTime( timestamp * 1000 );
820 return [
821 pad( d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate() ), 'T',
822 pad( d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ), 'Z'
823 ].join( '' );
824 }
825
826 /**
827 * Resolves dependencies and detects circular references.
828 *
829 * @private
830 * @param {string} module Name of the top-level module whose dependencies shall be
831 * resolved and sorted.
832 * @param {Array} resolved Returns a topological sort of the given module and its
833 * dependencies, such that later modules depend on earlier modules. The array
834 * contains the module names. If the array contains already some module names,
835 * this function appends its result to the pre-existing array.
836 * @param {Object} [unresolved] Hash used to track the current dependency
837 * chain; used to report loops in the dependency graph.
838 * @throws {Error} If any unregistered module or a dependency loop is encountered
839 */
840 function sortDependencies( module, resolved, unresolved ) {
841 var n, deps, len, skip;
842
843 if ( !hasOwn.call( registry, module ) ) {
844 throw new Error( 'Unknown dependency: ' + module );
845 }
846
847 if ( registry[module].skip !== null ) {
848 /*jshint evil:true */
849 skip = new Function( registry[module].skip );
850 registry[module].skip = null;
851 if ( skip() ) {
852 registry[module].skipped = true;
853 registry[module].dependencies = [];
854 registry[module].state = 'ready';
855 handlePending( module );
856 return;
857 }
858 }
859
860 // Resolves dynamic loader function and replaces it with its own results
861 if ( $.isFunction( registry[module].dependencies ) ) {
862 registry[module].dependencies = registry[module].dependencies();
863 // Ensures the module's dependencies are always in an array
864 if ( typeof registry[module].dependencies !== 'object' ) {
865 registry[module].dependencies = [registry[module].dependencies];
866 }
867 }
868 if ( $.inArray( module, resolved ) !== -1 ) {
869 // Module already resolved; nothing to do.
870 return;
871 }
872 // unresolved is optional, supply it if not passed in
873 if ( !unresolved ) {
874 unresolved = {};
875 }
876 // Tracks down dependencies
877 deps = registry[module].dependencies;
878 len = deps.length;
879 for ( n = 0; n < len; n += 1 ) {
880 if ( $.inArray( deps[n], resolved ) === -1 ) {
881 if ( unresolved[deps[n]] ) {
882 throw new Error(
883 'Circular reference detected: ' + module +
884 ' -> ' + deps[n]
885 );
886 }
887
888 // Add to unresolved
889 unresolved[module] = true;
890 sortDependencies( deps[n], resolved, unresolved );
891 delete unresolved[module];
892 }
893 }
894 resolved[resolved.length] = module;
895 }
896
897 /**
898 * Gets a list of module names that a module depends on in their proper dependency
899 * order.
900 *
901 * @private
902 * @param {string} module Module name or array of string module names
903 * @return {Array} list of dependencies, including 'module'.
904 * @throws {Error} If circular reference is detected
905 */
906 function resolve( module ) {
907 var m, resolved;
908
909 // Allow calling with an array of module names
910 if ( $.isArray( module ) ) {
911 resolved = [];
912 for ( m = 0; m < module.length; m += 1 ) {
913 sortDependencies( module[m], resolved );
914 }
915 return resolved;
916 }
917
918 if ( typeof module === 'string' ) {
919 resolved = [];
920 sortDependencies( module, resolved );
921 return resolved;
922 }
923
924 throw new Error( 'Invalid module argument: ' + module );
925 }
926
927 /**
928 * Narrows a list of module names down to those matching a specific
929 * state (see comment on top of this scope for a list of valid states).
930 * One can also filter for 'unregistered', which will return the
931 * modules names that don't have a registry entry.
932 *
933 * @private
934 * @param {string|string[]} states Module states to filter by
935 * @param {Array} [modules] List of module names to filter (optional, by default the entire
936 * registry is used)
937 * @return {Array} List of filtered module names
938 */
939 function filter( states, modules ) {
940 var list, module, s, m;
941
942 // Allow states to be given as a string
943 if ( typeof states === 'string' ) {
944 states = [states];
945 }
946 // If called without a list of modules, build and use a list of all modules
947 list = [];
948 if ( modules === undefined ) {
949 modules = [];
950 for ( module in registry ) {
951 modules[modules.length] = module;
952 }
953 }
954 // Build a list of modules which are in one of the specified states
955 for ( s = 0; s < states.length; s += 1 ) {
956 for ( m = 0; m < modules.length; m += 1 ) {
957 if ( !hasOwn.call( registry, modules[m] ) ) {
958 // Module does not exist
959 if ( states[s] === 'unregistered' ) {
960 // OK, undefined
961 list[list.length] = modules[m];
962 }
963 } else {
964 // Module exists, check state
965 if ( registry[modules[m]].state === states[s] ) {
966 // OK, correct state
967 list[list.length] = modules[m];
968 }
969 }
970 }
971 }
972 return list;
973 }
974
975 /**
976 * Determine whether all dependencies are in state 'ready', which means we may
977 * execute the module or job now.
978 *
979 * @private
980 * @param {Array} dependencies Dependencies (module names) to be checked.
981 * @return {boolean} True if all dependencies are in state 'ready', false otherwise
982 */
983 function allReady( dependencies ) {
984 return filter( 'ready', dependencies ).length === dependencies.length;
985 }
986
987 /**
988 * A module has entered state 'ready', 'error', or 'missing'. Automatically update pending jobs
989 * and modules that depend upon this module. if the given module failed, propagate the 'error'
990 * state up the dependency tree; otherwise, execute all jobs/modules that now have all their
991 * dependencies satisfied. On jobs depending on a failed module, run the error callback, if any.
992 *
993 * @private
994 * @param {string} module Name of module that entered one of the states 'ready', 'error', or 'missing'.
995 */
996 function handlePending( module ) {
997 var j, job, hasErrors, m, stateChange;
998
999 // Modules.
1000 if ( $.inArray( registry[module].state, ['error', 'missing'] ) !== -1 ) {
1001 // If the current module failed, mark all dependent modules also as failed.
1002 // Iterate until steady-state to propagate the error state upwards in the
1003 // dependency tree.
1004 do {
1005 stateChange = false;
1006 for ( m in registry ) {
1007 if ( $.inArray( registry[m].state, ['error', 'missing'] ) === -1 ) {
1008 if ( filter( ['error', 'missing'], registry[m].dependencies ).length > 0 ) {
1009 registry[m].state = 'error';
1010 stateChange = true;
1011 }
1012 }
1013 }
1014 } while ( stateChange );
1015 }
1016
1017 // Execute all jobs whose dependencies are either all satisfied or contain at least one failed module.
1018 for ( j = 0; j < jobs.length; j += 1 ) {
1019 hasErrors = filter( ['error', 'missing'], jobs[j].dependencies ).length > 0;
1020 if ( hasErrors || allReady( jobs[j].dependencies ) ) {
1021 // All dependencies satisfied, or some have errors
1022 job = jobs[j];
1023 jobs.splice( j, 1 );
1024 j -= 1;
1025 try {
1026 if ( hasErrors ) {
1027 if ( $.isFunction( job.error ) ) {
1028 job.error( new Error( 'Module ' + module + ' has failed dependencies' ), [module] );
1029 }
1030 } else {
1031 if ( $.isFunction( job.ready ) ) {
1032 job.ready();
1033 }
1034 }
1035 } catch ( e ) {
1036 // A user-defined callback raised an exception.
1037 // Swallow it to protect our state machine!
1038 log( 'Exception thrown by user callback', e );
1039 }
1040 }
1041 }
1042
1043 if ( registry[module].state === 'ready' ) {
1044 // The current module became 'ready'. Set it in the module store, and recursively execute all
1045 // dependent modules that are loaded and now have all dependencies satisfied.
1046 mw.loader.store.set( module, registry[module] );
1047 for ( m in registry ) {
1048 if ( registry[m].state === 'loaded' && allReady( registry[m].dependencies ) ) {
1049 execute( m );
1050 }
1051 }
1052 }
1053 }
1054
1055 /**
1056 * Adds a script tag to the DOM, either using document.write or low-level DOM manipulation,
1057 * depending on whether document-ready has occurred yet and whether we are in async mode.
1058 *
1059 * @private
1060 * @param {string} src URL to script, will be used as the src attribute in the script tag
1061 * @param {Function} [callback] Callback which will be run when the script is done
1062 * @param {boolean} [async=false] Whether to load modules asynchronously.
1063 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1064 */
1065 function addScript( src, callback, async ) {
1066 // Using isReady directly instead of storing it locally from a $().ready callback (bug 31895)
1067 if ( $.isReady || async ) {
1068 $.ajax( {
1069 url: src,
1070 dataType: 'script',
1071 // Force jQuery behaviour to be for crossDomain. Otherwise jQuery would use
1072 // XHR for a same domain request instead of <script>, which changes the request
1073 // headers (potentially missing a cache hit), and reduces caching in general
1074 // since browsers cache XHR much less (if at all). And XHR means we retreive
1075 // text, so we'd need to $.globalEval, which then messes up line numbers.
1076 crossDomain: true,
1077 cache: true,
1078 async: true
1079 } ).always( callback );
1080 } else {
1081 /*jshint evil:true */
1082 document.write( mw.html.element( 'script', { 'src': src }, '' ) );
1083 if ( callback ) {
1084 // Document.write is synchronous, so this is called when it's done.
1085 // FIXME: That's a lie. doc.write isn't actually synchronous.
1086 callback();
1087 }
1088 }
1089 }
1090
1091 /**
1092 * Executes a loaded module, making it ready to use
1093 *
1094 * @private
1095 * @param {string} module Module name to execute
1096 */
1097 function execute( module ) {
1098 var key, value, media, i, urls, cssHandle, checkCssHandles,
1099 cssHandlesRegistered = false;
1100
1101 if ( !hasOwn.call( registry, module ) ) {
1102 throw new Error( 'Module has not been registered yet: ' + module );
1103 } else if ( registry[module].state === 'registered' ) {
1104 throw new Error( 'Module has not been requested from the server yet: ' + module );
1105 } else if ( registry[module].state === 'loading' ) {
1106 throw new Error( 'Module has not completed loading yet: ' + module );
1107 } else if ( registry[module].state === 'ready' ) {
1108 throw new Error( 'Module has already been executed: ' + module );
1109 }
1110
1111 /**
1112 * Define loop-function here for efficiency
1113 * and to avoid re-using badly scoped variables.
1114 * @ignore
1115 */
1116 function addLink( media, url ) {
1117 var el = document.createElement( 'link' );
1118 // For IE: Insert in document *before* setting href
1119 getMarker().before( el );
1120 el.rel = 'stylesheet';
1121 if ( media && media !== 'all' ) {
1122 el.media = media;
1123 }
1124 // If you end up here from an IE exception "SCRIPT: Invalid property value.",
1125 // see #addEmbeddedCSS, bug 31676, and bug 47277 for details.
1126 el.href = url;
1127 }
1128
1129 function runScript() {
1130 var script, markModuleReady, nestedAddScript;
1131 try {
1132 script = registry[module].script;
1133 markModuleReady = function () {
1134 registry[module].state = 'ready';
1135 handlePending( module );
1136 };
1137 nestedAddScript = function ( arr, callback, async, i ) {
1138 // Recursively call addScript() in its own callback
1139 // for each element of arr.
1140 if ( i >= arr.length ) {
1141 // We're at the end of the array
1142 callback();
1143 return;
1144 }
1145
1146 addScript( arr[i], function () {
1147 nestedAddScript( arr, callback, async, i + 1 );
1148 }, async );
1149 };
1150
1151 if ( $.isArray( script ) ) {
1152 nestedAddScript( script, markModuleReady, registry[module].async, 0 );
1153 } else if ( $.isFunction( script ) ) {
1154 registry[module].state = 'ready';
1155 // Pass jQuery twice so that the signature of the closure which wraps
1156 // the script can bind both '$' and 'jQuery'.
1157 script( $, $ );
1158 handlePending( module );
1159 }
1160 } catch ( e ) {
1161 // This needs to NOT use mw.log because these errors are common in production mode
1162 // and not in debug mode, such as when a symbol that should be global isn't exported
1163 log( 'Exception thrown by ' + module, e );
1164 registry[module].state = 'error';
1165 handlePending( module );
1166 }
1167 }
1168
1169 // This used to be inside runScript, but since that is now fired asychronously
1170 // (after CSS is loaded) we need to set it here right away. It is crucial that
1171 // when execute() is called this is set synchronously, otherwise modules will get
1172 // executed multiple times as the registry will state that it isn't loading yet.
1173 registry[module].state = 'loading';
1174
1175 // Add localizations to message system
1176 if ( $.isPlainObject( registry[module].messages ) ) {
1177 mw.messages.set( registry[module].messages );
1178 }
1179
1180 // Initialise templates
1181 if ( registry[module].templates ) {
1182 mw.templates.set( module, registry[module].templates );
1183 }
1184
1185 if ( $.isReady || registry[module].async ) {
1186 // Make sure we don't run the scripts until all (potentially asynchronous)
1187 // stylesheet insertions have completed.
1188 ( function () {
1189 var pending = 0;
1190 checkCssHandles = function () {
1191 // cssHandlesRegistered ensures we don't take off too soon, e.g. when
1192 // one of the cssHandles is fired while we're still creating more handles.
1193 if ( cssHandlesRegistered && pending === 0 && runScript ) {
1194 runScript();
1195 runScript = undefined; // Revoke
1196 }
1197 };
1198 cssHandle = function () {
1199 var check = checkCssHandles;
1200 pending++;
1201 return function () {
1202 if ( check ) {
1203 pending--;
1204 check();
1205 check = undefined; // Revoke
1206 }
1207 };
1208 };
1209 }() );
1210 } else {
1211 // We are in blocking mode, and so we can't afford to wait for CSS
1212 cssHandle = function () {};
1213 // Run immediately
1214 checkCssHandles = runScript;
1215 }
1216
1217 // Process styles (see also mw.loader.implement)
1218 // * back-compat: { <media>: css }
1219 // * back-compat: { <media>: [url, ..] }
1220 // * { "css": [css, ..] }
1221 // * { "url": { <media>: [url, ..] } }
1222 if ( $.isPlainObject( registry[module].style ) ) {
1223 for ( key in registry[module].style ) {
1224 value = registry[module].style[key];
1225 media = undefined;
1226
1227 if ( key !== 'url' && key !== 'css' ) {
1228 // Backwards compatibility, key is a media-type
1229 if ( typeof value === 'string' ) {
1230 // back-compat: { <media>: css }
1231 // Ignore 'media' because it isn't supported (nor was it used).
1232 // Strings are pre-wrapped in "@media". The media-type was just ""
1233 // (because it had to be set to something).
1234 // This is one of the reasons why this format is no longer used.
1235 addEmbeddedCSS( value, cssHandle() );
1236 } else {
1237 // back-compat: { <media>: [url, ..] }
1238 media = key;
1239 key = 'bc-url';
1240 }
1241 }
1242
1243 // Array of css strings in key 'css',
1244 // or back-compat array of urls from media-type
1245 if ( $.isArray( value ) ) {
1246 for ( i = 0; i < value.length; i += 1 ) {
1247 if ( key === 'bc-url' ) {
1248 // back-compat: { <media>: [url, ..] }
1249 addLink( media, value[i] );
1250 } else if ( key === 'css' ) {
1251 // { "css": [css, ..] }
1252 addEmbeddedCSS( value[i], cssHandle() );
1253 }
1254 }
1255 // Not an array, but a regular object
1256 // Array of urls inside media-type key
1257 } else if ( typeof value === 'object' ) {
1258 // { "url": { <media>: [url, ..] } }
1259 for ( media in value ) {
1260 urls = value[media];
1261 for ( i = 0; i < urls.length; i += 1 ) {
1262 addLink( media, urls[i] );
1263 }
1264 }
1265 }
1266 }
1267 }
1268
1269 // Kick off.
1270 cssHandlesRegistered = true;
1271 checkCssHandles();
1272 }
1273
1274 /**
1275 * Adds a dependencies to the queue with optional callbacks to be run
1276 * when the dependencies are ready or fail
1277 *
1278 * @private
1279 * @param {string|string[]} dependencies Module name or array of string module names
1280 * @param {Function} [ready] Callback to execute when all dependencies are ready
1281 * @param {Function} [error] Callback to execute when any dependency fails
1282 * @param {boolean} [async=false] Whether to load modules asynchronously.
1283 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1284 */
1285 function request( dependencies, ready, error, async ) {
1286 var n;
1287
1288 // Allow calling by single module name
1289 if ( typeof dependencies === 'string' ) {
1290 dependencies = [dependencies];
1291 }
1292
1293 // Add ready and error callbacks if they were given
1294 if ( ready !== undefined || error !== undefined ) {
1295 jobs[jobs.length] = {
1296 'dependencies': filter(
1297 ['registered', 'loading', 'loaded'],
1298 dependencies
1299 ),
1300 'ready': ready,
1301 'error': error
1302 };
1303 }
1304
1305 // Queue up any dependencies that are registered
1306 dependencies = filter( ['registered'], dependencies );
1307 for ( n = 0; n < dependencies.length; n += 1 ) {
1308 if ( $.inArray( dependencies[n], queue ) === -1 ) {
1309 queue[queue.length] = dependencies[n];
1310 if ( async ) {
1311 // Mark this module as async in the registry
1312 registry[dependencies[n]].async = true;
1313 }
1314 }
1315 }
1316
1317 // Work the queue
1318 mw.loader.work();
1319 }
1320
1321 function sortQuery( o ) {
1322 var sorted = {}, key, a = [];
1323 for ( key in o ) {
1324 if ( hasOwn.call( o, key ) ) {
1325 a.push( key );
1326 }
1327 }
1328 a.sort();
1329 for ( key = 0; key < a.length; key += 1 ) {
1330 sorted[a[key]] = o[a[key]];
1331 }
1332 return sorted;
1333 }
1334
1335 /**
1336 * Converts a module map of the form { foo: [ 'bar', 'baz' ], bar: [ 'baz, 'quux' ] }
1337 * to a query string of the form foo.bar,baz|bar.baz,quux
1338 * @private
1339 */
1340 function buildModulesString( moduleMap ) {
1341 var arr = [], p, prefix;
1342 for ( prefix in moduleMap ) {
1343 p = prefix === '' ? '' : prefix + '.';
1344 arr.push( p + moduleMap[prefix].join( ',' ) );
1345 }
1346 return arr.join( '|' );
1347 }
1348
1349 /**
1350 * Asynchronously append a script tag to the end of the body
1351 * that invokes load.php
1352 * @private
1353 * @param {Object} moduleMap Module map, see #buildModulesString
1354 * @param {Object} currReqBase Object with other parameters (other than 'modules') to use in the request
1355 * @param {string} sourceLoadScript URL of load.php
1356 * @param {boolean} async Whether to load modules asynchronously.
1357 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1358 */
1359 function doRequest( moduleMap, currReqBase, sourceLoadScript, async ) {
1360 var request = $.extend(
1361 { modules: buildModulesString( moduleMap ) },
1362 currReqBase
1363 );
1364 request = sortQuery( request );
1365 // Append &* to avoid triggering the IE6 extension check
1366 addScript( sourceLoadScript + '?' + $.param( request ) + '&*', null, async );
1367 }
1368
1369 /**
1370 * Resolve indexed dependencies.
1371 *
1372 * ResourceLoader uses an optimization to save space which replaces module names in
1373 * dependency lists with the index of that module within the array of module
1374 * registration data if it exists. The benefit is a significant reduction in the data
1375 * size of the startup module. This function changes those dependency lists back to
1376 * arrays of strings.
1377 *
1378 * @param {Array} modules Modules array
1379 */
1380 function resolveIndexedDependencies( modules ) {
1381 var i, iLen, j, jLen, module, dependency;
1382
1383 // Expand indexed dependency names
1384 for ( i = 0, iLen = modules.length; i < iLen; i++ ) {
1385 module = modules[i];
1386 if ( module[2] ) {
1387 for ( j = 0, jLen = module[2].length; j < jLen; j++ ) {
1388 dependency = module[2][j];
1389 if ( typeof dependency === 'number' ) {
1390 module[2][j] = modules[dependency][0];
1391 }
1392 }
1393 }
1394 }
1395 }
1396
1397 /* Public Members */
1398 return {
1399 /**
1400 * The module registry is exposed as an aid for debugging and inspecting page
1401 * state; it is not a public interface for modifying the registry.
1402 *
1403 * @see #registry
1404 * @property
1405 * @private
1406 */
1407 moduleRegistry: registry,
1408
1409 /**
1410 * @inheritdoc #newStyleTag
1411 * @method
1412 */
1413 addStyleTag: newStyleTag,
1414
1415 /**
1416 * Batch-request queued dependencies from the server.
1417 */
1418 work: function () {
1419 var reqBase, splits, maxQueryLength, q, b, bSource, bGroup, bSourceGroup,
1420 source, concatSource, origBatch, group, g, i, modules, maxVersion, sourceLoadScript,
1421 currReqBase, currReqBaseLength, moduleMap, l,
1422 lastDotIndex, prefix, suffix, bytesAdded, async;
1423
1424 // Build a list of request parameters common to all requests.
1425 reqBase = {
1426 skin: mw.config.get( 'skin' ),
1427 lang: mw.config.get( 'wgUserLanguage' ),
1428 debug: mw.config.get( 'debug' )
1429 };
1430 // Split module batch by source and by group.
1431 splits = {};
1432 maxQueryLength = mw.config.get( 'wgResourceLoaderMaxQueryLength', -1 );
1433
1434 // Appends a list of modules from the queue to the batch
1435 for ( q = 0; q < queue.length; q += 1 ) {
1436 // Only request modules which are registered
1437 if ( hasOwn.call( registry, queue[q] ) && registry[queue[q]].state === 'registered' ) {
1438 // Prevent duplicate entries
1439 if ( $.inArray( queue[q], batch ) === -1 ) {
1440 batch[batch.length] = queue[q];
1441 // Mark registered modules as loading
1442 registry[queue[q]].state = 'loading';
1443 }
1444 }
1445 }
1446
1447 mw.loader.store.init();
1448 if ( mw.loader.store.enabled ) {
1449 concatSource = [];
1450 origBatch = batch;
1451 batch = $.grep( batch, function ( module ) {
1452 var source = mw.loader.store.get( module );
1453 if ( source ) {
1454 concatSource.push( source );
1455 return false;
1456 }
1457 return true;
1458 } );
1459 try {
1460 $.globalEval( concatSource.join( ';' ) );
1461 } catch ( err ) {
1462 // Not good, the cached mw.loader.implement calls failed! This should
1463 // never happen, barring ResourceLoader bugs, browser bugs and PEBKACs.
1464 // Depending on how corrupt the string is, it is likely that some
1465 // modules' implement() succeeded while the ones after the error will
1466 // never run and leave their modules in the 'loading' state forever.
1467
1468 // Since this is an error not caused by an individual module but by
1469 // something that infected the implement call itself, don't take any
1470 // risks and clear everything in this cache.
1471 mw.loader.store.clear();
1472 // Re-add the ones still pending back to the batch and let the server
1473 // repopulate these modules to the cache.
1474 // This means that at most one module will be useless (the one that had
1475 // the error) instead of all of them.
1476 log( 'Error while evaluating data from mw.loader.store', err );
1477 origBatch = $.grep( origBatch, function ( module ) {
1478 return registry[module].state === 'loading';
1479 } );
1480 batch = batch.concat( origBatch );
1481 }
1482 }
1483
1484 // Early exit if there's nothing to load...
1485 if ( !batch.length ) {
1486 return;
1487 }
1488
1489 // The queue has been processed into the batch, clear up the queue.
1490 queue = [];
1491
1492 // Always order modules alphabetically to help reduce cache
1493 // misses for otherwise identical content.
1494 batch.sort();
1495
1496 // Split batch by source and by group.
1497 for ( b = 0; b < batch.length; b += 1 ) {
1498 bSource = registry[batch[b]].source;
1499 bGroup = registry[batch[b]].group;
1500 if ( !hasOwn.call( splits, bSource ) ) {
1501 splits[bSource] = {};
1502 }
1503 if ( !hasOwn.call( splits[bSource], bGroup ) ) {
1504 splits[bSource][bGroup] = [];
1505 }
1506 bSourceGroup = splits[bSource][bGroup];
1507 bSourceGroup[bSourceGroup.length] = batch[b];
1508 }
1509
1510 // Clear the batch - this MUST happen before we append any
1511 // script elements to the body or it's possible that a script
1512 // will be locally cached, instantly load, and work the batch
1513 // again, all before we've cleared it causing each request to
1514 // include modules which are already loaded.
1515 batch = [];
1516
1517 for ( source in splits ) {
1518
1519 sourceLoadScript = sources[source];
1520
1521 for ( group in splits[source] ) {
1522
1523 // Cache access to currently selected list of
1524 // modules for this group from this source.
1525 modules = splits[source][group];
1526
1527 // Calculate the highest timestamp
1528 maxVersion = 0;
1529 for ( g = 0; g < modules.length; g += 1 ) {
1530 if ( registry[modules[g]].version > maxVersion ) {
1531 maxVersion = registry[modules[g]].version;
1532 }
1533 }
1534
1535 currReqBase = $.extend( { version: formatVersionNumber( maxVersion ) }, reqBase );
1536 // For user modules append a user name to the request.
1537 if ( group === 'user' && mw.config.get( 'wgUserName' ) !== null ) {
1538 currReqBase.user = mw.config.get( 'wgUserName' );
1539 }
1540 currReqBaseLength = $.param( currReqBase ).length;
1541 async = true;
1542 // We may need to split up the request to honor the query string length limit,
1543 // so build it piece by piece.
1544 l = currReqBaseLength + 9; // '&modules='.length == 9
1545
1546 moduleMap = {}; // { prefix: [ suffixes ] }
1547
1548 for ( i = 0; i < modules.length; i += 1 ) {
1549 // Determine how many bytes this module would add to the query string
1550 lastDotIndex = modules[i].lastIndexOf( '.' );
1551
1552 // If lastDotIndex is -1, substr() returns an empty string
1553 prefix = modules[i].substr( 0, lastDotIndex );
1554 suffix = modules[i].slice( lastDotIndex + 1 );
1555
1556 bytesAdded = hasOwn.call( moduleMap, prefix )
1557 ? suffix.length + 3 // '%2C'.length == 3
1558 : modules[i].length + 3; // '%7C'.length == 3
1559
1560 // If the request would become too long, create a new one,
1561 // but don't create empty requests
1562 if ( maxQueryLength > 0 && !$.isEmptyObject( moduleMap ) && l + bytesAdded > maxQueryLength ) {
1563 // This request would become too long, create a new one
1564 // and fire off the old one
1565 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1566 moduleMap = {};
1567 async = true;
1568 l = currReqBaseLength + 9;
1569 }
1570 if ( !hasOwn.call( moduleMap, prefix ) ) {
1571 moduleMap[prefix] = [];
1572 }
1573 moduleMap[prefix].push( suffix );
1574 if ( !registry[modules[i]].async ) {
1575 // If this module is blocking, make the entire request blocking
1576 // This is slightly suboptimal, but in practice mixing of blocking
1577 // and async modules will only occur in debug mode.
1578 async = false;
1579 }
1580 l += bytesAdded;
1581 }
1582 // If there's anything left in moduleMap, request that too
1583 if ( !$.isEmptyObject( moduleMap ) ) {
1584 doRequest( moduleMap, currReqBase, sourceLoadScript, async );
1585 }
1586 }
1587 }
1588 },
1589
1590 /**
1591 * Register a source.
1592 *
1593 * The #work method will use this information to split up requests by source.
1594 *
1595 * mw.loader.addSource( 'mediawikiwiki', '//www.mediawiki.org/w/load.php' );
1596 *
1597 * @param {string} id Short string representing a source wiki, used internally for
1598 * registered modules to indicate where they should be loaded from (usually lowercase a-z).
1599 * @param {Object|string} loadUrl load.php url, may be an object for backwards-compatibility
1600 * @return {boolean}
1601 */
1602 addSource: function ( id, loadUrl ) {
1603 var source;
1604 // Allow multiple additions
1605 if ( typeof id === 'object' ) {
1606 for ( source in id ) {
1607 mw.loader.addSource( source, id[source] );
1608 }
1609 return true;
1610 }
1611
1612 if ( hasOwn.call( sources, id ) ) {
1613 throw new Error( 'source already registered: ' + id );
1614 }
1615
1616 if ( typeof loadUrl === 'object' ) {
1617 loadUrl = loadUrl.loadScript;
1618 }
1619
1620 sources[id] = loadUrl;
1621
1622 return true;
1623 },
1624
1625 /**
1626 * Register a module, letting the system know about it and its
1627 * properties. Startup modules contain calls to this function.
1628 *
1629 * When using multiple module registration by passing an array, dependencies that
1630 * are specified as references to modules within the array will be resolved before
1631 * the modules are registered.
1632 *
1633 * @param {string|Array} module Module name or array of arrays, each containing
1634 * a list of arguments compatible with this method
1635 * @param {number} version Module version number as a timestamp (falls backs to 0)
1636 * @param {string|Array|Function} dependencies One string or array of strings of module
1637 * names on which this module depends, or a function that returns that array.
1638 * @param {string} [group=null] Group which the module is in
1639 * @param {string} [source='local'] Name of the source
1640 * @param {string} [skip=null] Script body of the skip function
1641 */
1642 register: function ( module, version, dependencies, group, source, skip ) {
1643 var i, len;
1644 // Allow multiple registration
1645 if ( typeof module === 'object' ) {
1646 resolveIndexedDependencies( module );
1647 for ( i = 0, len = module.length; i < len; i++ ) {
1648 // module is an array of module names
1649 if ( typeof module[i] === 'string' ) {
1650 mw.loader.register( module[i] );
1651 // module is an array of arrays
1652 } else if ( typeof module[i] === 'object' ) {
1653 mw.loader.register.apply( mw.loader, module[i] );
1654 }
1655 }
1656 return;
1657 }
1658 // Validate input
1659 if ( typeof module !== 'string' ) {
1660 throw new Error( 'module must be a string, not a ' + typeof module );
1661 }
1662 if ( hasOwn.call( registry, module ) ) {
1663 throw new Error( 'module already registered: ' + module );
1664 }
1665 // List the module as registered
1666 registry[module] = {
1667 version: version !== undefined ? parseInt( version, 10 ) : 0,
1668 dependencies: [],
1669 group: typeof group === 'string' ? group : null,
1670 source: typeof source === 'string' ? source : 'local',
1671 state: 'registered',
1672 skip: typeof skip === 'string' ? skip : null
1673 };
1674 if ( typeof dependencies === 'string' ) {
1675 // Allow dependencies to be given as a single module name
1676 registry[module].dependencies = [ dependencies ];
1677 } else if ( typeof dependencies === 'object' || $.isFunction( dependencies ) ) {
1678 // Allow dependencies to be given as an array of module names
1679 // or a function which returns an array
1680 registry[module].dependencies = dependencies;
1681 }
1682 },
1683
1684 /**
1685 * Implement a module given the components that make up the module.
1686 *
1687 * When #load or #using requests one or more modules, the server
1688 * response contain calls to this function.
1689 *
1690 * All arguments are required.
1691 *
1692 * @param {string} module Name of module
1693 * @param {Function|Array} script Function with module code or Array of URLs to
1694 * be used as the src attribute of a new `<script>` tag.
1695 * @param {Object} style Should follow one of the following patterns:
1696 *
1697 * { "css": [css, ..] }
1698 * { "url": { <media>: [url, ..] } }
1699 *
1700 * And for backwards compatibility (needs to be supported forever due to caching):
1701 *
1702 * { <media>: css }
1703 * { <media>: [url, ..] }
1704 *
1705 * The reason css strings are not concatenated anymore is bug 31676. We now check
1706 * whether it's safe to extend the stylesheet (see #canExpandStylesheetWith).
1707 *
1708 * @param {Object} msgs List of key/value pairs to be added to mw#messages.
1709 * @param {Object} [templates] List of key/value pairs to be added to mw#templates.
1710 */
1711 implement: function ( module, script, style, msgs, templates ) {
1712 // Validate input
1713 if ( typeof module !== 'string' ) {
1714 throw new Error( 'module must be a string, not a ' + typeof module );
1715 }
1716 if ( !$.isFunction( script ) && !$.isArray( script ) ) {
1717 throw new Error( 'script must be a function or an array, not a ' + typeof script );
1718 }
1719 if ( !$.isPlainObject( style ) ) {
1720 throw new Error( 'style must be an object, not a ' + typeof style );
1721 }
1722 if ( !$.isPlainObject( msgs ) ) {
1723 throw new Error( 'msgs must be an object, not a ' + typeof msgs );
1724 }
1725 if ( templates !== undefined && !$.isPlainObject( templates ) ) {
1726 throw new Error( 'templates must be an object, not a ' + typeof templates );
1727 }
1728 // Automatically register module
1729 if ( !hasOwn.call( registry, module ) ) {
1730 mw.loader.register( module );
1731 }
1732 // Check for duplicate implementation
1733 if ( hasOwn.call( registry, module ) && registry[module].script !== undefined ) {
1734 throw new Error( 'module already implemented: ' + module );
1735 }
1736 // Attach components
1737 registry[module].script = script;
1738 registry[module].style = style;
1739 registry[module].messages = msgs;
1740 // Templates are optional (for back-compat)
1741 registry[module].templates = templates || {};
1742 // The module may already have been marked as erroneous
1743 if ( $.inArray( registry[module].state, ['error', 'missing'] ) === -1 ) {
1744 registry[module].state = 'loaded';
1745 if ( allReady( registry[module].dependencies ) ) {
1746 execute( module );
1747 }
1748 }
1749 },
1750
1751 /**
1752 * Execute a function as soon as one or more required modules are ready.
1753 *
1754 * Example of inline dependency on OOjs:
1755 *
1756 * mw.loader.using( 'oojs', function () {
1757 * OO.compare( [ 1 ], [ 1 ] );
1758 * } );
1759 *
1760 * @param {string|Array} dependencies Module name or array of modules names the callback
1761 * dependends on to be ready before executing
1762 * @param {Function} [ready] Callback to execute when all dependencies are ready
1763 * @param {Function} [error] Callback to execute if one or more dependencies failed
1764 * @return {jQuery.Promise}
1765 * @since 1.23 this returns a promise
1766 */
1767 using: function ( dependencies, ready, error ) {
1768 var deferred = $.Deferred();
1769
1770 // Allow calling with a single dependency as a string
1771 if ( typeof dependencies === 'string' ) {
1772 dependencies = [ dependencies ];
1773 } else if ( !$.isArray( dependencies ) ) {
1774 // Invalid input
1775 throw new Error( 'Dependencies must be a string or an array' );
1776 }
1777
1778 if ( ready ) {
1779 deferred.done( ready );
1780 }
1781 if ( error ) {
1782 deferred.fail( error );
1783 }
1784
1785 // Resolve entire dependency map
1786 dependencies = resolve( dependencies );
1787 if ( allReady( dependencies ) ) {
1788 // Run ready immediately
1789 deferred.resolve();
1790 } else if ( filter( ['error', 'missing'], dependencies ).length ) {
1791 // Execute error immediately if any dependencies have errors
1792 deferred.reject(
1793 new Error( 'One or more dependencies failed to load' ),
1794 dependencies
1795 );
1796 } else {
1797 // Not all dependencies are ready: queue up a request
1798 request( dependencies, deferred.resolve, deferred.reject );
1799 }
1800
1801 return deferred.promise();
1802 },
1803
1804 /**
1805 * Load an external script or one or more modules.
1806 *
1807 * @param {string|Array} modules Either the name of a module, array of modules,
1808 * or a URL of an external script or style
1809 * @param {string} [type='text/javascript'] MIME type to use if calling with a URL of an
1810 * external script or style; acceptable values are "text/css" and
1811 * "text/javascript"; if no type is provided, text/javascript is assumed.
1812 * @param {boolean} [async] Whether to load modules asynchronously.
1813 * Ignored (and defaulted to `true`) if the document-ready event has already occurred.
1814 * Defaults to `true` if loading a URL, `false` otherwise.
1815 */
1816 load: function ( modules, type, async ) {
1817 var filtered, m, module, l;
1818
1819 // Validate input
1820 if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
1821 throw new Error( 'modules must be a string or an array, not a ' + typeof modules );
1822 }
1823 // Allow calling with an external url or single dependency as a string
1824 if ( typeof modules === 'string' ) {
1825 // Support adding arbitrary external scripts
1826 if ( /^(https?:)?\/\//.test( modules ) ) {
1827 if ( async === undefined ) {
1828 // Assume async for bug 34542
1829 async = true;
1830 }
1831 if ( type === 'text/css' ) {
1832 // IE7-8 throws security warnings when inserting a <link> tag
1833 // with a protocol-relative URL set though attributes (instead of
1834 // properties) - when on HTTPS. See also bug 41331.
1835 l = document.createElement( 'link' );
1836 l.rel = 'stylesheet';
1837 l.href = modules;
1838 $( 'head' ).append( l );
1839 return;
1840 }
1841 if ( type === 'text/javascript' || type === undefined ) {
1842 addScript( modules, null, async );
1843 return;
1844 }
1845 // Unknown type
1846 throw new Error( 'invalid type for external url, must be text/css or text/javascript. not ' + type );
1847 }
1848 // Called with single module
1849 modules = [ modules ];
1850 }
1851
1852 // Filter out undefined modules, otherwise resolve() will throw
1853 // an exception for trying to load an undefined module.
1854 // Undefined modules are acceptable here in load(), because load() takes
1855 // an array of unrelated modules, whereas the modules passed to
1856 // using() are related and must all be loaded.
1857 for ( filtered = [], m = 0; m < modules.length; m += 1 ) {
1858 if ( hasOwn.call( registry, modules[m] ) ) {
1859 module = registry[modules[m]];
1860 if ( $.inArray( module.state, ['error', 'missing'] ) === -1 ) {
1861 filtered[filtered.length] = modules[m];
1862 }
1863 }
1864 }
1865
1866 if ( filtered.length === 0 ) {
1867 return;
1868 }
1869 // Resolve entire dependency map
1870 filtered = resolve( filtered );
1871 // If all modules are ready, nothing to be done
1872 if ( allReady( filtered ) ) {
1873 return;
1874 }
1875 // If any modules have errors: also quit.
1876 if ( filter( ['error', 'missing'], filtered ).length ) {
1877 return;
1878 }
1879 // Since some modules are not yet ready, queue up a request.
1880 request( filtered, undefined, undefined, async );
1881 },
1882
1883 /**
1884 * Change the state of one or more modules.
1885 *
1886 * @param {string|Object} module Module name or object of module name/state pairs
1887 * @param {string} state State name
1888 */
1889 state: function ( module, state ) {
1890 var m;
1891
1892 if ( typeof module === 'object' ) {
1893 for ( m in module ) {
1894 mw.loader.state( m, module[m] );
1895 }
1896 return;
1897 }
1898 if ( !hasOwn.call( registry, module ) ) {
1899 mw.loader.register( module );
1900 }
1901 if ( $.inArray( state, ['ready', 'error', 'missing'] ) !== -1
1902 && registry[module].state !== state ) {
1903 // Make sure pending modules depending on this one get executed if their
1904 // dependencies are now fulfilled!
1905 registry[module].state = state;
1906 handlePending( module );
1907 } else {
1908 registry[module].state = state;
1909 }
1910 },
1911
1912 /**
1913 * Get the version of a module.
1914 *
1915 * @param {string} module Name of module
1916 * @return {string|null} The version, or null if the module (or its version) is not
1917 * in the registry.
1918 */
1919 getVersion: function ( module ) {
1920 if ( !hasOwn.call( registry, module ) || registry[module].version === undefined ) {
1921 return null;
1922 }
1923 return formatVersionNumber( registry[module].version );
1924 },
1925
1926 /**
1927 * Get the state of a module.
1928 *
1929 * @param {string} module Name of module
1930 * @return {string|null} The state, or null if the module (or its version) is not
1931 * in the registry.
1932 */
1933 getState: function ( module ) {
1934 if ( !hasOwn.call( registry, module ) || registry[module].state === undefined ) {
1935 return null;
1936 }
1937 return registry[module].state;
1938 },
1939
1940 /**
1941 * Get the names of all registered modules.
1942 *
1943 * @return {Array}
1944 */
1945 getModuleNames: function () {
1946 return $.map( registry, function ( i, key ) {
1947 return key;
1948 } );
1949 },
1950
1951 /**
1952 * @inheritdoc mw.inspect#runReports
1953 * @method
1954 */
1955 inspect: function () {
1956 var args = slice.call( arguments );
1957 mw.loader.using( 'mediawiki.inspect', function () {
1958 mw.inspect.runReports.apply( mw.inspect, args );
1959 } );
1960 },
1961
1962 /**
1963 * On browsers that implement the localStorage API, the module store serves as a
1964 * smart complement to the browser cache. Unlike the browser cache, the module store
1965 * can slice a concatenated response from ResourceLoader into its constituent
1966 * modules and cache each of them separately, using each module's versioning scheme
1967 * to determine when the cache should be invalidated.
1968 *
1969 * @singleton
1970 * @class mw.loader.store
1971 */
1972 store: {
1973 // Whether the store is in use on this page.
1974 enabled: null,
1975
1976 // The contents of the store, mapping '[module name]@[version]' keys
1977 // to module implementations.
1978 items: {},
1979
1980 // Cache hit stats
1981 stats: { hits: 0, misses: 0, expired: 0 },
1982
1983 /**
1984 * Construct a JSON-serializable object representing the content of the store.
1985 * @return {Object} Module store contents.
1986 */
1987 toJSON: function () {
1988 return { items: mw.loader.store.items, vary: mw.loader.store.getVary() };
1989 },
1990
1991 /**
1992 * Get the localStorage key for the entire module store. The key references
1993 * $wgDBname to prevent clashes between wikis which share a common host.
1994 *
1995 * @return {string} localStorage item key
1996 */
1997 getStoreKey: function () {
1998 return 'MediaWikiModuleStore:' + mw.config.get( 'wgDBname' );
1999 },
2000
2001 /**
2002 * Get a string key on which to vary the module cache.
2003 * @return {string} String of concatenated vary conditions.
2004 */
2005 getVary: function () {
2006 return [
2007 mw.config.get( 'skin' ),
2008 mw.config.get( 'wgResourceLoaderStorageVersion' ),
2009 mw.config.get( 'wgUserLanguage' )
2010 ].join( ':' );
2011 },
2012
2013 /**
2014 * Get a string key for a specific module. The key format is '[name]@[version]'.
2015 *
2016 * @param {string} module Module name
2017 * @return {string|null} Module key or null if module does not exist
2018 */
2019 getModuleKey: function ( module ) {
2020 return hasOwn.call( registry, module ) ?
2021 ( module + '@' + registry[module].version ) : null;
2022 },
2023
2024 /**
2025 * Initialize the store.
2026 *
2027 * Retrieves store from localStorage and (if successfully retrieved) decoding
2028 * the stored JSON value to a plain object.
2029 *
2030 * The try / catch block is used for JSON & localStorage feature detection.
2031 * See the in-line documentation for Modernizr's localStorage feature detection
2032 * code for a full account of why we need a try / catch:
2033 * <https://github.com/Modernizr/Modernizr/blob/v2.7.1/modernizr.js#L771-L796>.
2034 */
2035 init: function () {
2036 var raw, data;
2037
2038 if ( mw.loader.store.enabled !== null ) {
2039 // Init already ran
2040 return;
2041 }
2042
2043 if ( !mw.config.get( 'wgResourceLoaderStorageEnabled' ) ) {
2044 // Disabled by configuration.
2045 // Clear any previous store to free up space. (T66721)
2046 mw.loader.store.clear();
2047 mw.loader.store.enabled = false;
2048 return;
2049 }
2050 if ( mw.config.get( 'debug' ) ) {
2051 // Disable module store in debug mode
2052 mw.loader.store.enabled = false;
2053 return;
2054 }
2055
2056 try {
2057 raw = localStorage.getItem( mw.loader.store.getStoreKey() );
2058 // If we get here, localStorage is available; mark enabled
2059 mw.loader.store.enabled = true;
2060 data = JSON.parse( raw );
2061 if ( data && typeof data.items === 'object' && data.vary === mw.loader.store.getVary() ) {
2062 mw.loader.store.items = data.items;
2063 return;
2064 }
2065 } catch ( e ) {
2066 log( 'Storage error', e );
2067 }
2068
2069 if ( raw === undefined ) {
2070 // localStorage failed; disable store
2071 mw.loader.store.enabled = false;
2072 } else {
2073 mw.loader.store.update();
2074 }
2075 },
2076
2077 /**
2078 * Retrieve a module from the store and update cache hit stats.
2079 *
2080 * @param {string} module Module name
2081 * @return {string|boolean} Module implementation or false if unavailable
2082 */
2083 get: function ( module ) {
2084 var key;
2085
2086 if ( !mw.loader.store.enabled ) {
2087 return false;
2088 }
2089
2090 key = mw.loader.store.getModuleKey( module );
2091 if ( key in mw.loader.store.items ) {
2092 mw.loader.store.stats.hits++;
2093 return mw.loader.store.items[key];
2094 }
2095 mw.loader.store.stats.misses++;
2096 return false;
2097 },
2098
2099 /**
2100 * Stringify a module and queue it for storage.
2101 *
2102 * @param {string} module Module name
2103 * @param {Object} descriptor The module's descriptor as set in the registry
2104 */
2105 set: function ( module, descriptor ) {
2106 var args, key;
2107
2108 if ( !mw.loader.store.enabled ) {
2109 return false;
2110 }
2111
2112 key = mw.loader.store.getModuleKey( module );
2113
2114 if (
2115 // Already stored a copy of this exact version
2116 key in mw.loader.store.items ||
2117 // Module failed to load
2118 descriptor.state !== 'ready' ||
2119 // Unversioned, private, or site-/user-specific
2120 ( !descriptor.version || $.inArray( descriptor.group, [ 'private', 'user', 'site' ] ) !== -1 ) ||
2121 // Partial descriptor
2122 $.inArray( undefined, [ descriptor.script, descriptor.style,
2123 descriptor.messages, descriptor.templates ] ) !== -1
2124 ) {
2125 // Decline to store
2126 return false;
2127 }
2128
2129 try {
2130 args = [
2131 JSON.stringify( module ),
2132 typeof descriptor.script === 'function' ?
2133 String( descriptor.script ) :
2134 JSON.stringify( descriptor.script ),
2135 JSON.stringify( descriptor.style ),
2136 JSON.stringify( descriptor.messages ),
2137 JSON.stringify( descriptor.templates )
2138 ];
2139 // Attempted workaround for a possible Opera bug (bug 57567).
2140 // This regex should never match under sane conditions.
2141 if ( /^\s*\(/.test( args[1] ) ) {
2142 args[1] = 'function' + args[1];
2143 log( 'Detected malformed function stringification (bug 57567)' );
2144 }
2145 } catch ( e ) {
2146 log( 'Storage error', e );
2147 return;
2148 }
2149
2150 mw.loader.store.items[key] = 'mw.loader.implement(' + args.join( ',' ) + ');';
2151 mw.loader.store.update();
2152 },
2153
2154 /**
2155 * Iterate through the module store, removing any item that does not correspond
2156 * (in name and version) to an item in the module registry.
2157 */
2158 prune: function () {
2159 var key, module;
2160
2161 if ( !mw.loader.store.enabled ) {
2162 return false;
2163 }
2164
2165 for ( key in mw.loader.store.items ) {
2166 module = key.slice( 0, key.indexOf( '@' ) );
2167 if ( mw.loader.store.getModuleKey( module ) !== key ) {
2168 mw.loader.store.stats.expired++;
2169 delete mw.loader.store.items[key];
2170 }
2171 }
2172 },
2173
2174 /**
2175 * Clear the entire module store right now.
2176 */
2177 clear: function () {
2178 mw.loader.store.items = {};
2179 localStorage.removeItem( mw.loader.store.getStoreKey() );
2180 },
2181
2182 /**
2183 * Sync modules to localStorage.
2184 *
2185 * This function debounces localStorage updates. When called multiple times in
2186 * quick succession, the calls are coalesced into a single update operation.
2187 * This allows us to call #update without having to consider the module load
2188 * queue; the call to localStorage.setItem will be naturally deferred until the
2189 * page is quiescent.
2190 *
2191 * Because localStorage is shared by all pages with the same origin, if multiple
2192 * pages are loaded with different module sets, the possibility exists that
2193 * modules saved by one page will be clobbered by another. But the impact would
2194 * be minor and the problem would be corrected by subsequent page views.
2195 *
2196 * @method
2197 */
2198 update: ( function () {
2199 var timer;
2200
2201 function flush() {
2202 var data,
2203 key = mw.loader.store.getStoreKey();
2204
2205 if ( !mw.loader.store.enabled ) {
2206 return false;
2207 }
2208 mw.loader.store.prune();
2209 try {
2210 // Replacing the content of the module store might fail if the new
2211 // contents would exceed the browser's localStorage size limit. To
2212 // avoid clogging the browser with stale data, always remove the old
2213 // value before attempting to set the new one.
2214 localStorage.removeItem( key );
2215 data = JSON.stringify( mw.loader.store );
2216 localStorage.setItem( key, data );
2217 } catch ( e ) {
2218 log( 'Storage error', e );
2219 }
2220 }
2221
2222 return function () {
2223 clearTimeout( timer );
2224 timer = setTimeout( flush, 2000 );
2225 };
2226 }() )
2227 }
2228 };
2229 }() ),
2230
2231 /**
2232 * HTML construction helper functions
2233 *
2234 * @example
2235 *
2236 * var Html, output;
2237 *
2238 * Html = mw.html;
2239 * output = Html.element( 'div', {}, new Html.Raw(
2240 * Html.element( 'img', { src: '<' } )
2241 * ) );
2242 * mw.log( output ); // <div><img src="&lt;"/></div>
2243 *
2244 * @class mw.html
2245 * @singleton
2246 */
2247 html: ( function () {
2248 function escapeCallback( s ) {
2249 switch ( s ) {
2250 case '\'':
2251 return '&#039;';
2252 case '"':
2253 return '&quot;';
2254 case '<':
2255 return '&lt;';
2256 case '>':
2257 return '&gt;';
2258 case '&':
2259 return '&amp;';
2260 }
2261 }
2262
2263 return {
2264 /**
2265 * Escape a string for HTML.
2266 *
2267 * Converts special characters to HTML entities.
2268 *
2269 * mw.html.escape( '< > \' & "' );
2270 * // Returns &lt; &gt; &#039; &amp; &quot;
2271 *
2272 * @param {string} s The string to escape
2273 * @return {string} HTML
2274 */
2275 escape: function ( s ) {
2276 return s.replace( /['"<>&]/g, escapeCallback );
2277 },
2278
2279 /**
2280 * Create an HTML element string, with safe escaping.
2281 *
2282 * @param {string} name The tag name.
2283 * @param {Object} attrs An object with members mapping element names to values
2284 * @param {Mixed} contents The contents of the element. May be either:
2285 *
2286 * - string: The string is escaped.
2287 * - null or undefined: The short closing form is used, e.g. `<br/>`.
2288 * - this.Raw: The value attribute is included without escaping.
2289 * - this.Cdata: The value attribute is included, and an exception is
2290 * thrown if it contains an illegal ETAGO delimiter.
2291 * See <http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2>.
2292 * @return {string} HTML
2293 */
2294 element: function ( name, attrs, contents ) {
2295 var v, attrName, s = '<' + name;
2296
2297 for ( attrName in attrs ) {
2298 v = attrs[attrName];
2299 // Convert name=true, to name=name
2300 if ( v === true ) {
2301 v = attrName;
2302 // Skip name=false
2303 } else if ( v === false ) {
2304 continue;
2305 }
2306 s += ' ' + attrName + '="' + this.escape( String( v ) ) + '"';
2307 }
2308 if ( contents === undefined || contents === null ) {
2309 // Self close tag
2310 s += '/>';
2311 return s;
2312 }
2313 // Regular open tag
2314 s += '>';
2315 switch ( typeof contents ) {
2316 case 'string':
2317 // Escaped
2318 s += this.escape( contents );
2319 break;
2320 case 'number':
2321 case 'boolean':
2322 // Convert to string
2323 s += String( contents );
2324 break;
2325 default:
2326 if ( contents instanceof this.Raw ) {
2327 // Raw HTML inclusion
2328 s += contents.value;
2329 } else if ( contents instanceof this.Cdata ) {
2330 // CDATA
2331 if ( /<\/[a-zA-z]/.test( contents.value ) ) {
2332 throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
2333 }
2334 s += contents.value;
2335 } else {
2336 throw new Error( 'mw.html.element: Invalid type of contents' );
2337 }
2338 }
2339 s += '</' + name + '>';
2340 return s;
2341 },
2342
2343 /**
2344 * Wrapper object for raw HTML passed to mw.html.element().
2345 * @class mw.html.Raw
2346 */
2347 Raw: function ( value ) {
2348 this.value = value;
2349 },
2350
2351 /**
2352 * Wrapper object for CDATA element contents passed to mw.html.element()
2353 * @class mw.html.Cdata
2354 */
2355 Cdata: function ( value ) {
2356 this.value = value;
2357 }
2358 };
2359 }() ),
2360
2361 // Skeleton user object. mediawiki.user.js extends this
2362 user: {
2363 options: new Map(),
2364 tokens: new Map()
2365 },
2366
2367 /**
2368 * Registry and firing of events.
2369 *
2370 * MediaWiki has various interface components that are extended, enhanced
2371 * or manipulated in some other way by extensions, gadgets and even
2372 * in core itself.
2373 *
2374 * This framework helps streamlining the timing of when these other
2375 * code paths fire their plugins (instead of using document-ready,
2376 * which can and should be limited to firing only once).
2377 *
2378 * Features like navigating to other wiki pages, previewing an edit
2379 * and editing itself – without a refresh – can then retrigger these
2380 * hooks accordingly to ensure everything still works as expected.
2381 *
2382 * Example usage:
2383 *
2384 * mw.hook( 'wikipage.content' ).add( fn ).remove( fn );
2385 * mw.hook( 'wikipage.content' ).fire( $content );
2386 *
2387 * Handlers can be added and fired for arbitrary event names at any time. The same
2388 * event can be fired multiple times. The last run of an event is memorized
2389 * (similar to `$(document).ready` and `$.Deferred().done`).
2390 * This means if an event is fired, and a handler added afterwards, the added
2391 * function will be fired right away with the last given event data.
2392 *
2393 * Like Deferreds and Promises, the mw.hook object is both detachable and chainable.
2394 * Thus allowing flexible use and optimal maintainability and authority control.
2395 * You can pass around the `add` and/or `fire` method to another piece of code
2396 * without it having to know the event name (or `mw.hook` for that matter).
2397 *
2398 * var h = mw.hook( 'bar.ready' );
2399 * new mw.Foo( .. ).fetch( { callback: h.fire } );
2400 *
2401 * Note: Events are documented with an underscore instead of a dot in the event
2402 * name due to jsduck not supporting dots in that position.
2403 *
2404 * @class mw.hook
2405 */
2406 hook: ( function () {
2407 var lists = {};
2408
2409 /**
2410 * Create an instance of mw.hook.
2411 *
2412 * @method hook
2413 * @member mw
2414 * @param {string} name Name of hook.
2415 * @return {mw.hook}
2416 */
2417 return function ( name ) {
2418 var list = hasOwn.call( lists, name ) ?
2419 lists[name] :
2420 lists[name] = $.Callbacks( 'memory' );
2421
2422 return {
2423 /**
2424 * Register a hook handler
2425 * @param {Function...} handler Function to bind.
2426 * @chainable
2427 */
2428 add: list.add,
2429
2430 /**
2431 * Unregister a hook handler
2432 * @param {Function...} handler Function to unbind.
2433 * @chainable
2434 */
2435 remove: list.remove,
2436
2437 /**
2438 * Run a hook.
2439 * @param {Mixed...} data
2440 * @chainable
2441 */
2442 fire: function () {
2443 return list.fireWith.call( this, null, slice.call( arguments ) );
2444 }
2445 };
2446 };
2447 }() )
2448 };
2449
2450 // Alias $j to jQuery for backwards compatibility
2451 // @deprecated since 1.23 Use $ or jQuery instead
2452 mw.log.deprecate( window, '$j', $, 'Use $ or jQuery instead.' );
2453
2454 // Attach to window and globally alias
2455 window.mw = window.mediaWiki = mw;
2456
2457 // Auto-register from pre-loaded startup scripts
2458 if ( $.isFunction( window.startUp ) ) {
2459 window.startUp();
2460 window.startUp = undefined;
2461 }
2462
2463 }( jQuery ) );