2 * JavaScript Backwards Compatibility
5 // Make calling .indexOf() on an array work on older browsers
6 if ( typeof Array
.prototype.indexOf
=== 'undefined' ) {
7 Array
.prototype.indexOf = function( needle
) {
8 for ( var i
= 0; i
< this.length
; i
++ ) {
9 if ( this[i
] === needle
) {
18 * Core MediaWiki JavaScript Library
24 // This will not change until we are 100% ready to turn off legacy globals
25 const LEGACY_GLOBALS
= true;
29 this.legacy
= LEGACY_GLOBALS
? window
: {};
34 * Log a string msg to the console
36 * All mw.log statements will be removed on minification so lots of mw.log calls will not impact performance in non-debug
37 * mode. This is done using simple regular expressions, so the input of this function needs to not contain things like a
38 * self-executing closure. In the case that the browser does not have a console available, one is created by appending a
39 * <div> element to the bottom of the body and then appending a <div> element to that for each message. In the case that
40 * the browser does have a console available
42 * @author Michael Dale <mdale@wikimedia.org>, Trevor Parscal <tparscal@wikimedia.org>
43 * @param {String} string String to output to console
45 this.log = function( string
) {
46 // Allow log messages to use a configured prefix
47 if ( mw
.config
.exists( 'mw.log.prefix' ) ) {
48 string
= mw
.config
.get( 'mw.log.prefix' ) + string
;
50 // Try to use an existing console
51 if ( typeof window
.console
!== 'undefined' && typeof window
.console
.log
== 'function' ) {
52 window
.console
.log( string
);
54 // Show a log box for console-less browsers
55 var $log
= $( '#mw_log_console' );
57 $log
= $( '<div id="mw_log_console"></div>' )
59 'position': 'absolute',
66 'background-color': 'white',
67 'border-top': 'solid 1px #DDDDDD'
69 .appendTo( $( 'body' ) );
73 $( '<div>' + string
+ '</div>' )
75 'border-bottom': 'solid 1px #DDDDDD',
77 'font-family': 'monospace',
78 'padding': '0.125em 0.25em'
85 * An object which allows single and multiple existence, setting and getting on a list of key / value pairs
87 this.config
= new ( function() {
92 // List of configuration values - in legacy mode these configurations were ALL in the global space
93 var values
= LEGACY_GLOBALS
? window
: {};
98 * Sets one or multiple configuration values using a key and a value or an object of keys and values
100 this.set = function( keys
, value
) {
101 if ( typeof keys
=== 'object' ) {
102 for ( var key
in keys
) {
103 values
[key
] = keys
[key
];
105 } else if ( typeof keys
=== 'string' && typeof value
!== 'undefined' ) {
106 values
[keys
] = value
;
110 * Gets one or multiple configuration values using a key and an optional fallback or an array of keys
112 this.get = function( keys
, fallback
) {
113 if ( typeof keys
=== 'object' ) {
115 for ( var k
= 0; k
< keys
.length
; k
++ ) {
116 if ( typeof values
[keys
[k
]] !== 'undefined' ) {
117 result
[keys
[k
]] = values
[keys
[k
]];
121 } else if ( typeof values
[keys
] === 'undefined' ) {
122 return typeof fallback
!== 'undefined' ? fallback
: null;
128 * Checks if one or multiple configuration fields exist
130 this.exists = function( keys
) {
131 if ( typeof keys
=== 'object' ) {
132 for ( var k
= 0; k
< keys
.length
; k
++ ) {
133 if ( !( keys
[k
] in values
) ) {
139 return keys
in values
;
144 * Localization system
146 this.msg
= new ( function() {
148 /* Private Members */
151 // List of localized messages
156 this.set = function( keys
, value
) {
157 if ( typeof keys
=== 'object' ) {
158 for ( var key
in keys
) {
159 messages
[key
] = keys
[key
];
161 } else if ( typeof keys
=== 'string' && typeof value
!== 'undefined' ) {
162 messages
[keys
] = value
;
165 this.get = function( key
, args
) {
166 if ( !( key
in messages
) ) {
167 return '<' + key
+ '>';
169 var msg
= messages
[key
];
170 if ( typeof args
== 'object' || typeof args
== 'array' ) {
171 for ( var argKey
in args
) {
172 msg
= msg
.replace( '\$' + ( parseInt( argKey
) + 1 ), args
[argKey
] );
174 } else if ( typeof args
== 'string' || typeof args
== 'number' ) {
175 msg
= msg
.replace( '$1', args
);
181 * Client-side module loader which integrates with the MediaWiki ResourceLoader
183 this.loader
= new ( function() {
185 /* Private Members */
188 var server
= 'load.php';
190 * Mapping of registered modules
195 * 'needs': ['required module', 'required module', ...],
196 * 'state': 'registered, loading, loaded, or ready',
197 * 'script': function() {},
198 * 'style': 'css code string',
199 * 'localization': { 'key': 'value' }
204 // List of callbacks waiting on dependent modules to be loaded so they can be executed
206 // Until document ready, load requests will be collected in a batch queue
208 // True after document ready occurs
211 /* Private Methods */
214 * Gets a list of modules names that a module needs in their proper dependency order
216 * @param string module name
218 * @throws Error if circular reference is detected
220 function needs( module
) {
221 if ( !( module
in registry
) ) {
222 // Undefined modules have no needs
227 if ( arguments
.length
=== 3 ) {
228 // Use arguemnts on inner call
229 resolved
= arguments
[1];
230 unresolved
= arguments
[2];
232 unresolved
[unresolved
.length
] = module
;
233 for ( n
in registry
[module
].needs
) {
234 if ( resolved
.indexOf( registry
[module
].needs
[n
] ) === -1 ) {
235 if ( unresolved
.indexOf( registry
[module
].needs
[n
] ) !== -1 ) {
237 'Circular reference detected: ' + module
+ ' -> ' + registry
[module
].needs
[n
]
240 needs( registry
[module
].needs
[n
], resolved
, unresolved
);
243 resolved
[resolved
.length
] = module
;
244 unresolved
.slice( unresolved
.indexOf( module
), 1 );
245 if ( arguments
.length
=== 1 ) {
246 // Return resolved list on outer call
251 * Narrows a list of module names down to those matching a specific state. Possible states are 'undefined',
252 * 'registered', 'loading', 'loaded', or 'ready'
254 * @param mixed string or array of strings of module states to filter by
255 * @param array list of module names to filter (optional, all modules will be used by default)
256 * @return array list of filtered module names
258 function filter( states
, modules
) {
260 if ( typeof modules
=== 'undefined' ) {
262 for ( module
in registry
) {
263 modules
[modules
.length
] = module
;
266 for ( var s
in states
) {
267 for ( var m
in modules
) {
269 ( states
[s
] == 'undefined' && typeof registry
[modules
[m
]] === 'undefined' ) ||
270 ( typeof registry
[modules
[m
]] === 'object' && registry
[modules
[m
]].state
=== states
[s
] )
272 list
[list
.length
] = modules
[m
];
279 * Executes a loaded module, making it ready to use
281 * @param string module name to execute
283 function execute( module
) {
284 if ( typeof registry
[module
] === 'undefined' ) {
285 throw new Error( 'module has not been registered: ' + module
);
287 switch ( registry
[module
].state
) {
289 throw new Error( 'module has not completed loading: ' + module
);
292 throw new Error( 'module has not completed loading: ' + module
);
295 throw new Error( 'module has already been loaded: ' + module
);
298 // Add style sheet to document
299 if ( typeof registry
[module
].style
=== 'string' && registry
[module
].style
.length
) {
300 $( 'head' ).append( '<style type="text/css">' + registry
[module
].style
+ '</style>' );
302 // Add localizations to message system
303 if ( typeof registry
[module
].localization
=== 'object' ) {
304 mw
.msg
.set( registry
[module
].localization
);
308 registry
[module
].script();
310 mw
.log( 'Exception thrown by ' + module
+ ': ' + e
.message
);
313 registry
[module
].state
= 'ready';
315 // Execute all modules which were waiting for this to be ready
316 for ( r
in registry
) {
317 if ( registry
[r
].state
== 'loaded' ) {
318 if ( filter( ['ready'], registry
[r
].needs
).length
== registry
[r
].needs
.length
) {
325 * Adds a callback and it's needs to the queue
327 * @param array list of module names the callback needs to be ready before being executed
328 * @param function callback to execute when needs are met
330 function request( needs
, callback
) {
331 queue
[queue
.length
] = { 'needs': filter( ['undefined', 'registered'], needs
), 'callback': callback
};
337 * Processes the queue, loading and executing when things when ready.
339 this.work = function() {
340 // Appends a list of modules to the batch
341 function append( modules
) {
342 for ( m
in modules
) {
343 // Prevent requesting modules which are loading, loaded or ready
344 if ( modules
[m
] in registry
&& registry
[modules
[m
]].state
== 'registered' ) {
345 // Since the batch can live between calls to work until document ready, we need to make sure
346 // we aren't making a duplicate entry
347 if ( batch
.indexOf( modules
[m
] ) == -1 ) {
348 batch
[batch
.length
] = modules
[m
];
349 registry
[modules
[m
]].state
= 'loading';
354 // Fill batch with modules that need to be loaded
355 for ( var q
in queue
) {
356 append( queue
[q
].needs
);
357 for ( n
in queue
[q
].needs
) {
358 append( needs( queue
[q
].needs
[n
] ) );
361 // After document ready, handle the batch
362 if ( ready
&& batch
.length
) {
363 // Always order modules alphabetically to help reduce cache misses for otherwise identical content
366 var base
= $.extend( {},
367 // Pass configuration values through the URL
368 mw
.config
.get( [ 'user', 'skin', 'space', 'view', 'language' ] ),
369 // Ensure request comes back in the proper mode (debug or not)
370 { 'debug': typeof mw
.debug
!== 'undefined' ? '1' : '0' }
373 if ( base
.debug
== '1' ) {
375 requests
[requests
.length
] = $.extend( { 'modules': batch
[b
] }, base
);
378 requests
[requests
.length
] = $.extend( { 'modules': batch
.join( '|' ) }, base
);
380 // It may be more performant to do this with an Ajax call, but that's limited to same-domain, so we
381 // can either auto-detect (if there really is any benefit) or just use this method, which is safe
382 setTimeout( function() {
383 // Clear the batch - this MUST happen before we append the script element to the body or it's
384 // possible that the script will be locally cached, instantly load, and work the batch again,
385 // all before we've cleared it causing each request to include modules which are already loaded
388 for ( r
in requests
) {
389 // Build out the HTML
390 var src
= mw
.util
.buildUrlString( {
391 'path': mw
.config
.get( 'wgScriptPath' ) + '/load.php',
394 html
+= '<script type="text/javascript" src="' + src
+ '"></script>';
396 // Append script to head
397 $( 'head' ).append( html
);
402 * Registers a module, letting the system know about it and it's dependencies. loader.js files contain calls
405 this.register = function( name
, needs
) {
407 if ( typeof name
!== 'string' ) {
408 throw new Error( 'name must be a string, not a ' + typeof name
);
410 if ( typeof registry
[name
] !== 'undefined' ) {
411 throw new Error( 'module already implemeneted: ' + name
);
413 // List the module as registered
414 registry
[name
] = { 'state': 'registered', 'needs': [] };
415 // Allow needs to be given as a function which returns a string or array
416 if ( typeof needs
=== 'function' ) {
419 if ( typeof needs
=== 'string' ) {
420 // Allow needs to be given as a single module name
421 registry
[name
].needs
= [needs
];
422 } else if ( typeof needs
=== 'object' ) {
423 // Allow needs to be given as an array of module names
424 registry
[name
].needs
= needs
;
428 * Implements a module, giving the system a course of action to take upon loading. Results of a request for
429 * one or more modules contain calls to this function.
431 this.implement = function( name
, script
, style
, localization
) {
432 // Automaically register module
433 if ( typeof registry
[name
] === 'undefined' ) {
434 that
.register( name
, needs
);
437 if ( typeof script
!== 'function' ) {
438 throw new Error( 'script must be a function, not a ' + typeof script
);
440 if ( typeof style
!== 'undefined' && typeof style
!== 'string' ) {
441 throw new Error( 'style must be a string, not a ' + typeof style
);
443 if ( typeof localization
!== 'undefined' && typeof localization
!== 'object' ) {
444 throw new Error( 'localization must be an object, not a ' + typeof localization
);
446 if ( typeof registry
[name
] !== 'undefined' && typeof registry
[name
].script
!== 'undefined' ) {
447 throw new Error( 'module already implemeneted: ' + name
);
449 // Mark module as loaded
450 registry
[name
].state
= 'loaded';
452 registry
[name
].script
= script
;
453 if ( typeof style
=== 'string' ) {
454 registry
[name
].style
= style
;
456 if ( typeof localization
=== 'object' ) {
457 registry
[name
].localization
= localization
;
459 // Execute or queue callback
460 if ( filter( ['ready'], registry
[name
].needs
).length
== registry
[name
].needs
.length
) {
463 request( registry
[name
].needs
, function() { execute( name
); } );
467 * Executes a function as soon as one or more required modules are ready
469 * @param mixed string or array of strings of modules names the callback needs to be ready before executing
470 * @param function callback to execute when all needs are met
472 this.using = function( needs
, callback
) {
474 if ( typeof needs
!== 'object' && typeof needs
!== 'string' ) {
475 throw new Error( 'needs must be a string or an array, not a ' + typeof needs
)
477 if ( typeof callback
!== 'function' ) {
478 throw new Error( 'callback must be a function, not a ' + typeof callback
)
480 if ( typeof needs
=== 'string' ) {
483 // Execute or queue callback
484 if ( filter( ['ready'], needs
).length
== needs
.length
) {
487 request( needs
, callback
);
493 $( document
).ready( function() {
499 * General purpose utilities
501 this.util
= new ( function() {
503 /* Private Members */
506 // Decoded user agent string cache
512 * Builds a url string from an object containing any of the following components:
516 * server "www.domain.com"
517 * path "path/to/my/file.html"
518 * query "this=thåt" or { 'this': 'thåt' }
519 * fragment "place_on_the_page"
521 * Results in: "http://www.domain.com/path/to/my/file.html?this=th%C3%A5t#place_on_the_page"
523 * All arguments to this function are assumed to be URL-encoded already, except for the
524 * query parameter if provided in object form.
526 this.buildUrlString = function( components
) {
528 if ( typeof components
.scheme
=== 'string' ) {
529 url
+= components
.scheme
+ '://';
531 if ( typeof components
.server
=== 'string' ) {
532 url
+= components
.server
+ '/';
534 if ( typeof components
.path
=== 'string' ) {
535 url
+= components
.path
;
537 if ( typeof components
.query
=== 'string' ) {
538 url
+= '?' + components
.query
;
539 } else if ( typeof components
.query
=== 'object' ) {
540 url
+= '?' + that
.buildQueryString( components
.query
);
542 if ( typeof components
.fragment
=== 'string' ) {
543 url
+= '#' + components
.fragment
;
548 * RFC 3986 compliant URI component encoder - with identical behavior as PHP's urlencode function. Note: PHP's
549 * urlencode function prior to version 5.3 also escapes tildes, this does not. The naming here is not the same
550 * as PHP because PHP can't decide out to name things (underscores sometimes?), much less set a reasonable
551 * precedence for how things should be named in other environments. We use camelCase and action-subject here.
553 this.encodeUrlComponent = function( string
) {
554 return encodeURIComponent( new String( string
) )
555 .replace(/!/g
, '%21')
556 .replace(/'/g, '%27')
557 .replace(/\(/g, '%28')
558 .replace(/\)/g, '%29')
559 .replace(/\*/g, '%2A
')
560 .replace(/%20/g, '+');
563 * Builds a query string from an object with key and values
565 this.buildQueryString = function( parameters ) {
566 if ( typeof parameters === 'object
' ) {
568 for ( var p in parameters ) {
569 parts[parts.length] = that.encodeUrlComponent( p ) + '=' + that.encodeUrlComponent( parameters[p] );
571 return parts.join( '&' );
576 * Returns an object containing information about the browser
578 * The resulting client object will be in the following format:
583 * 'version
': '3.5.1',
584 * 'versionBase
': '3',
585 * 'versionNumber
': 3.5,
588 this.client = function() {
589 // Use the cached version if possible
590 if ( client === null ) {
594 // Name of browsers or layout engines we don't recognize
596 // Generic version digit
598 // Strings found in user agent strings that need to be conformed
599 var wildUserAgents
= [ 'Opera', 'Navigator', 'Minefield', 'KHTML', 'Chrome', 'PLAYSTATION 3'];
600 // Translations for conforming user agent strings
601 var userAgentTranslations
= [
602 // Tons of browsers lie about being something they are not
603 [/(Firefox|MSIE|KHTML,\slike\sGecko|Konqueror)/, ''],
604 // Chrome lives in the shadow of Safari still
605 ['Chrome Safari', 'Chrome'],
606 // KHTML is the layout engine not the browser - LIES!
607 ['KHTML', 'Konqueror'],
608 // Firefox nightly builds
609 ['Minefield', 'Firefox'],
610 // This helps keep differnt versions consistent
611 ['Navigator', 'Netscape'],
612 // This prevents version extraction issues, otherwise translation would happen later
613 ['PLAYSTATION 3', 'PS3'],
615 // Strings which precede a version number in a user agent string - combined and used as match 1 in
616 // version detectection
617 var versionPrefixes
= [
618 'camino', 'chrome', 'firefox', 'netscape', 'netscape6', 'opera', 'version', 'konqueror', 'lynx',
619 'msie', 'safari', 'ps3'
621 // Used as matches 2, 3 and 4 in version extraction - 3 is used as actual version number
622 var versionSuffix
= '(\/|\;?\s|)([a-z0-9\.\+]*?)(\;|dev|rel|\\)|\s|$)';
623 // Names of known browsers
625 'camino', 'chrome', 'firefox', 'netscape', 'konqueror', 'lynx', 'msie', 'opera', 'safari', 'ipod',
626 'iphone', 'blackberry', 'ps3'
628 // Tanslations for conforming browser names
629 var browserTranslations
= [];
630 // Names of known layout engines
631 var layoutNames
= ['gecko', 'konqueror', 'msie', 'opera', 'webkit'];
632 // Translations for conforming layout names
633 var layoutTranslations
= [['konqueror', 'khtml'], ['msie', 'trident'], ['opera', 'presto']];
634 // Names of known operating systems
635 var osNames
= ['win', 'mac', 'linux', 'sunos', 'solaris', 'iphone'];
636 // Translations for conforming operating system names
637 var osTranslations
= [['sunos', 'solaris']];
641 // Performs multiple replacements on a string
642 function translate( source
, translations
) {
643 for ( var i
= 0; i
< translations
.length
; i
++ ) {
644 source
= source
.replace( translations
[i
][0], translations
[i
][1] );
651 var userAgent
= navigator
.userAgent
, match
, browser
= uk
, layout
= uk
, os
= uk
, version
= x
;
652 if ( match
= new RegExp( '(' + wildUserAgents
.join( '|' ) + ')' ).exec( userAgent
) ) {
653 // Takes a userAgent string and translates given text into something we can more easily work with
654 userAgent
= translate( userAgent
, userAgentTranslations
);
656 // Everything will be in lowercase from now on
657 userAgent
= userAgent
.toLowerCase();
661 if ( match
= new RegExp( '(' + browserNames
.join( '|' ) + ')' ).exec( userAgent
) ) {
662 browser
= translate( match
[1], browserTranslations
);
664 if ( match
= new RegExp( '(' + layoutNames
.join( '|' ) + ')' ).exec( userAgent
) ) {
665 layout
= translate( match
[1], layoutTranslations
);
667 if ( match
= new RegExp( '(' + osNames
.join( '|' ) + ')' ).exec( navigator
.platform
.toLowerCase() ) ) {
668 var os
= translate( match
[1], osTranslations
);
670 if ( match
= new RegExp( '(' + versionPrefixes
.join( '|' ) + ')' + versionSuffix
).exec( userAgent
) ) {
674 /* Edge Cases -- did I mention about how user agent string lie? */
676 // Decode Safari's crazy 400+ version numbers
677 if ( name
.match( /safari/ ) && version
> 400 ) {
680 // Expose Opera 10's lies about being Opera 9.8
681 if ( name
=== 'opera' && version
>= 9.8) {
682 version
= userAgent
.match( /version\/([0-9\.]*)/i )[1] || 10;
692 'versionBase': ( version
!== x
? new String( version
).substr( 0, 1 ) : x
),
693 'versionNumber': ( parseFloat( version
, 10 ) || 0.0 )
699 * Checks the current browser against a support map object to determine if the browser has been black-listed or
700 * not. If the browser was not configured specifically it is assumed to work. It is assumed that the body
701 * element is classified as either "ltr" or "rtl". If neither is set, "ltr" is assumed.
703 * A browser map is in the following format:
706 * // Multiple rules with configurable operators
707 * 'msie': [['>=', 7], ['!=', 9]],
708 * // Blocked entirely
712 * // Test against a string
713 * 'msie': [['!==', '8.1.2.3']],
714 * // RTL rules do not fall through to LTR rules, you must explicity set each of them
719 * @param map Object of browser support map
721 * @return Boolean true if browser known or assumed to be supported, false if blacklisted
723 this.testClient = function( map
) {
724 var client
= this.client();
725 // Check over each browser condition to determine if we are running in a compatible client
726 var browser
= map
[$( 'body' ).is( '.rtl' ) ? 'rtl' : 'ltr'][client
.browser
];
727 if ( typeof browser
!== 'object' ) {
728 // Unknown, so we assume it's working
731 for ( var condition
in browser
) {
732 var op
= browser
[condition
][0];
733 var val
= browser
[condition
][1];
734 if ( val
=== false ) {
736 } else if ( typeof val
== 'string' ) {
737 if ( !( eval( 'client.version' + op
+ '"' + val
+ '"' ) ) ) {
740 } else if ( typeof val
== 'number' ) {
741 if ( !( eval( 'client.versionNumber' + op
+ val
) ) ) {
750 window
.mw
= $.extend( 'mw' in window
? window
.mw
: {}, this );