2 * JavaScript backwards-compatibility and support
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
) {
16 // Add array comparison functionality
17 if ( typeof Array
.prototype.compare
=== 'undefined' ) {
18 Array
.prototype.compare = function( against
) {
19 if ( this.length
!= against
.length
) {
22 for ( var i
= 0; i
< against
.length
; i
++ ) {
23 if ( this[i
].compare
) {
24 if ( !this[i
].compare( against
[i
] ) ) {
28 if ( this[i
] !== against
[i
] ) {
37 * Core MediaWiki JavaScript Library
40 window
.mediaWiki
= new ( function( $ ) {
44 // This will not change until we are 100% ready to turn off legacy globals
45 var LEGACY_GLOBALS
= true;
55 * An object which allows single and multiple get/set/exists functionality on a list of key / value pairs
57 * @param {boolean} global whether to get/set/exists values on the window object or a private object
58 * @param {function} parser function to perform extra processing before while getting a value which accepts
59 * value and options parameters where value is a string to be parsed and options is an object of options for the
62 'configuration': function( global
, parser
) {
67 var values
= global
=== true ? window
: {};
72 * Gets one or more values
74 * If called with no arguments, all values will be returned. If a parser is in use, no parsing will take
75 * place when calling with no arguments or calling with an array of names.
77 * @param {mixed} selection string name of value to get, array of string names of values to get, or object
78 * of name/option pairs
79 * @param {object} options optional set of options which are also passed to a parser if in use; only used
80 * when selection is a string
83 * // Value to use if key does not exist
87 this.get = function( selection
, options
) {
88 if ( typeof selection
=== 'object' ) {
90 for ( s
in selection
) {
91 if ( selection
.hasOwnProperty( s
) ) {
92 if ( typeof s
=== 'string' ) {
93 return that
.get( values
[s
], selection
[s
] );
95 return that
.get( selection
[s
] );
100 } else if ( typeof selection
=== 'string' ) {
101 if ( typeof values
[selection
] === 'undefined' ) {
102 return typeof options
=== 'object' && 'fallback' in options
?
103 options
.fallback
: '<' + selection
+ '>';
105 if ( typeof parser
=== 'function' ) {
106 return parser( values
[selection
], options
);
108 return values
[selection
];
117 * Sets one or multiple configuration values using a key and a value or an object of keys and values
119 * @param {mixed} key string of name by which value will be made accessible, or object of name/value pairs
120 * @param {mixed} value optional value to set, only in use when key is a string
122 this.set = function( selection
, value
) {
123 if ( typeof selection
=== 'object' ) {
124 for ( var s
in selection
) {
125 values
[s
] = selection
[s
];
127 } else if ( typeof selection
=== 'string' && typeof value
!== 'undefined' ) {
128 values
[selection
] = value
;
133 * Checks if one or multiple configuration fields exist
135 this.exists = function( selection
) {
136 if ( typeof keys
=== 'object' ) {
137 for ( var s
= 0; s
< selection
.length
; s
++ ) {
138 if ( !( selection
[s
] in values
) ) {
144 return selection
in values
;
153 * Dummy function which in debug mode can be replaced with a function that does something clever
155 this.log = function() { };
158 * List of configuration values
160 * In legacy mode the values this object wraps will be in the global space
162 this.config
= new this.prototypes
.configuration( LEGACY_GLOBALS
);
165 * Information about the current user
167 this.user
= new ( function() {
171 this.options
= new that
.prototypes
.configuration();
175 * Basic parser, can be replaced with something more robust
177 this.parser = function( text
, options
) {
178 if ( typeof options
=== 'object' && typeof options
.parameters
=== 'object' ) {
179 for ( var p
= 0; p
< options
.parameters
.length
; p
++ ) {
180 text
= text
.replace( '\$' + ( parseInt( p
) + 1 ), options
.parameters
[p
] );
187 * Localization system
189 this.msg
= new that
.prototypes
.configuration( false, this.parser
);
192 * Client-side module loader which integrates with the MediaWiki ResourceLoader
194 this.loader
= new ( function() {
196 /* Private Members */
200 * Mapping of registered modules
202 * The jquery module is pre-registered, because it must have already been provided for this object to have
203 * been built, and in debug mode jquery would have been provided through a unique loader request, making it
204 * impossible to hold back registration of jquery until after mediawiki.
209 * 'dependencies': ['required module', 'required module', ...], (or) function() {}
210 * 'state': 'registered', 'loading', 'loaded', 'ready', or 'error'
211 * 'script': function() {},
212 * 'style': 'css code string',
213 * 'messages': { 'key': 'value' },
214 * 'version': ############## (unix timestamp)
219 // List of modules which will be loaded as when ready
221 // List of modules to be loaded
223 // List of callback functions waiting for modules to be ready to be called
225 // Flag indicating that requests should be suspended
226 var suspended
= true;
227 // Flag inidicating that document ready has occured
230 /* Private Methods */
233 * Generates an ISO8601 string from a UNIX timestamp
235 function formatVersionNumber( timestamp
) {
236 var date
= new Date();
237 date
.setTime( timestamp
* 1000 );
239 return n
< 10 ? '0' + n
: n
242 return n
< 10 ? '00' + n
: ( n
< 100 ? '0' + n
: n
);
244 return date
.getUTCFullYear() + '-' +
245 pad1( date
.getUTCMonth() + 1 ) + '-' +
246 pad1( date
.getUTCDate() ) + 'T' +
247 pad1( date
.getUTCHours() ) + ':' +
248 pad1( date
.getUTCMinutes() ) + ':' +
249 pad1( date
.getUTCSeconds() ) +
254 * Recursively resolves dependencies and detects circular references
256 function recurse( module
, resolved
, unresolved
) {
257 unresolved
[unresolved
.length
] = module
;
258 // Resolves dynamic loader function and replaces it with it's own results
259 if ( typeof registry
[module
].dependencies
=== 'function' ) {
260 registry
[module
].dependencies
= registry
[module
].dependencies();
261 // Gaurantees the module's dependencies are always in an array
262 if ( typeof registry
[module
].dependencies
!== 'object' ) {
263 registry
[module
].dependencies
= [registry
[module
].dependencies
];
266 // Tracks down dependencies
267 for ( var n
= 0; n
< registry
[module
].dependencies
.length
; n
++ ) {
268 if ( resolved
.indexOf( registry
[module
].dependencies
[n
] ) === -1 ) {
269 if ( unresolved
.indexOf( registry
[module
].dependencies
[n
] ) !== -1 ) {
271 'Circular reference detected: ' + module
+ ' -> ' + registry
[module
].dependencies
[n
]
274 recurse( registry
[module
].dependencies
[n
], resolved
, unresolved
);
277 resolved
[resolved
.length
] = module
;
278 unresolved
.splice( unresolved
.indexOf( module
), 1 );
282 * Gets a list of modules names that a module dependencies in their proper dependency order
284 * @param mixed string module name or array of string module names
285 * @return list of dependencies
286 * @throws Error if circular reference is detected
288 function resolve( module
, resolved
, unresolved
) {
289 // Allow calling with an array of module names
290 if ( typeof module
=== 'object' ) {
292 for ( var m
= 0; m
< module
.length
; m
++ ) {
293 var dependencies
= resolve( module
[m
] );
294 for ( var n
= 0; n
< dependencies
.length
; n
++ ) {
295 modules
[modules
.length
] = dependencies
[n
];
299 } else if ( typeof module
=== 'string' ) {
300 // Undefined modules have no dependencies
301 if ( !( module
in registry
) ) {
305 recurse( module
, resolved
, [] );
308 throw new Error( 'Invalid module argument: ' + module
);
312 * Narrows a list of module names down to those matching a specific state. Possible states are 'undefined',
313 * 'registered', 'loading', 'loaded', or 'ready'
315 * @param mixed string or array of strings of module states to filter by
316 * @param array list of module names to filter (optional, all modules will be used by default)
317 * @return array list of filtered module names
319 function filter( states
, modules
) {
320 // Allow states to be given as a string
321 if ( typeof states
=== 'string' ) {
324 // If called without a list of modules, build and use a list of all modules
326 if ( typeof modules
=== 'undefined' ) {
328 for ( module
in registry
) {
329 modules
[modules
.length
] = module
;
332 // Build a list of modules which are in one of the specified states
333 for ( var s
= 0; s
< states
.length
; s
++ ) {
334 for ( var m
= 0; m
< modules
.length
; m
++ ) {
336 ( states
[s
] == 'undefined' && typeof registry
[modules
[m
]] === 'undefined' ) ||
337 ( typeof registry
[modules
[m
]] === 'object' && registry
[modules
[m
]].state
=== states
[s
] )
339 list
[list
.length
] = modules
[m
];
347 * Executes a loaded module, making it ready to use
349 * @param string module name to execute
351 function execute( module
) {
352 if ( typeof registry
[module
] === 'undefined' ) {
353 throw new Error( 'Module has not been registered yet: ' + module
);
354 } else if ( registry
[module
].state
=== 'registered' ) {
355 throw new Error( 'Module has not been requested from the server yet: ' + module
);
356 } else if ( registry
[module
].state
=== 'loading' ) {
357 throw new Error( 'Module has not completed loading yet: ' + module
);
358 } else if ( registry
[module
].state
=== 'ready' ) {
359 throw new Error( 'Module has already been loaded: ' + module
);
361 // Add style sheet to document
362 if ( typeof registry
[module
].style
=== 'string' && registry
[module
].style
.length
) {
363 $( 'head' ).append( '<style type="text/css">' + registry
[module
].style
+ '</style>' );
364 } else if ( typeof registry
[module
].style
=== 'object' && !( registry
[module
].style
instanceof Array
) ) {
365 for ( var media
in registry
[module
].style
) {
367 '<style type="text/css" media="' + media
+ '">' + registry
[module
].style
[media
] + '</style>'
371 // Add localizations to message system
372 if ( typeof registry
[module
].messages
=== 'object' ) {
373 mediaWiki
.msg
.set( registry
[module
].messages
);
377 registry
[module
].script();
378 registry
[module
].state
= 'ready';
379 // Run jobs who's dependencies have just been met
380 for ( var j
= 0; j
< jobs
.length
; j
++ ) {
381 if ( filter( 'ready', jobs
[j
].dependencies
).compare( jobs
[j
].dependencies
) ) {
382 if ( typeof jobs
[j
].ready
=== 'function' ) {
389 // Execute modules who's dependencies have just been met
390 for ( r
in registry
) {
391 if ( registry
[r
].state
== 'loaded' ) {
392 if ( filter( ['ready'], registry
[r
].dependencies
).compare( registry
[r
].dependencies
) ) {
398 mediaWiki
.log( 'Exception thrown by ' + module
+ ': ' + e
.message
);
400 registry
[module
].state
= 'error';
401 // Run error callbacks of jobs affected by this condition
402 for ( var j
= 0; j
< jobs
.length
; j
++ ) {
403 if ( jobs
[j
].dependencies
.indexOf( module
) !== -1 ) {
404 if ( typeof jobs
[j
].error
=== 'function' ) {
415 * Adds a dependencies to the queue with optional callbacks to be run when the dependencies are ready or fail
417 * @param mixed string moulde name or array of string module names
418 * @param function ready callback to execute when all dependencies are ready
419 * @param function error callback to execute when any dependency fails
421 function request( dependencies
, ready
, error
) {
422 // Allow calling by single module name
423 if ( typeof dependencies
=== 'string' ) {
424 dependencies
= [dependencies
];
425 if ( dependencies
[0] in registry
) {
426 for ( var n
= 0; n
< registry
[dependencies
[0]].dependencies
.length
; n
++ ) {
427 dependencies
[dependencies
.length
] = registry
[dependencies
[0]].dependencies
[n
];
431 // Add ready and error callbacks if they were given
432 if ( arguments
.length
> 1 ) {
433 jobs
[jobs
.length
] = {
434 'dependencies': filter( ['undefined', 'registered', 'loading', 'loaded'], dependencies
),
439 // Queue up any dependencies that are undefined or registered
440 dependencies
= filter( ['undefined', 'registered'], dependencies
);
441 for ( var n
= 0; n
< dependencies
.length
; n
++ ) {
442 if ( queue
.indexOf( dependencies
[n
] ) === -1 ) {
443 queue
[queue
.length
] = dependencies
[n
];
450 function sortQuery(o
) {
451 var sorted
= {}, key
, a
= [];
453 if ( o
.hasOwnProperty( key
) ) {
458 for ( key
= 0; key
< a
.length
; key
++ ) {
459 sorted
[a
[key
]] = o
[a
[key
]];
467 * Requests dependencies from server, loading and executing when things when ready.
469 this.work = function() {
470 // Appends a list of modules to the batch
471 for ( var q
= 0; q
< queue
.length
; q
++ ) {
472 // Only request modules which are undefined or registered
473 if ( !( queue
[q
] in registry
) || registry
[queue
[q
]].state
== 'registered' ) {
474 // Prevent duplicate entries
475 if ( batch
.indexOf( queue
[q
] ) === -1 ) {
476 batch
[batch
.length
] = queue
[q
];
477 // Mark registered modules as loading
478 if ( queue
[q
] in registry
) {
479 registry
[queue
[q
]].state
= 'loading';
484 // Clean up the queue
486 // After document ready, handle the batch
487 if ( !suspended
&& batch
.length
) {
488 // Always order modules alphabetically to help reduce cache misses for otherwise identical content
490 // Build a list of request parameters
492 'skin': mediaWiki
.config
.get( 'skin' ),
493 'lang': mediaWiki
.config
.get( 'wgUserLanguage' ),
494 'debug': mediaWiki
.config
.get( 'debug' )
496 // Extend request parameters with a list of modules in the batch
498 if ( base
.debug
== '1' ) {
499 for ( var b
= 0; b
< batch
.length
; b
++ ) {
500 requests
[requests
.length
] = $.extend(
501 { 'modules': batch
[b
], 'version': registry
[batch
[b
]].version
}, base
505 // Calculate the highest timestamp
507 for ( var b
= 0; b
< batch
.length
; b
++ ) {
508 if ( registry
[batch
[b
]].version
> version
) {
509 version
= registry
[batch
[b
]].version
;
512 requests
[requests
.length
] = $.extend(
513 { 'modules': batch
.join( '|' ), 'version': formatVersionNumber( version
) }, base
516 // Clear the batch - this MUST happen before we append the script element to the body or it's
517 // possible that the script will be locally cached, instantly load, and work the batch again,
518 // all before we've cleared it causing each request to include modules which are already loaded
520 // Asynchronously append a script tag to the end of the body
523 for ( var r
= 0; r
< requests
.length
; r
++ ) {
524 requests
[r
] = sortQuery( requests
[r
] );
525 // Build out the HTML
526 var src
= mediaWiki
.config
.get( 'wgLoadScript' ) + '?' + $.param( requests
[r
] );
527 html
+= '<script type="text/javascript" src="' + src
+ '"></script>';
531 // Load asynchronously after doumument ready
533 setTimeout( function() { $( 'body' ).append( request() ); }, 0 )
535 document
.write( request() );
541 * Registers a module, letting the system know about it and it's dependencies. loader.js files contain calls
544 this.register = function( module
, version
, dependencies
, group
) {
545 // Allow multiple registration
546 if ( typeof module
=== 'object' ) {
547 for ( var m
= 0; m
< module
.length
; m
++ ) {
548 if ( typeof module
[m
] === 'string' ) {
549 that
.register( module
[m
] );
550 } else if ( typeof module
[m
] === 'object' ) {
551 that
.register
.apply( that
, module
[m
] );
557 if ( typeof module
!== 'string' ) {
558 throw new Error( 'module must be a string, not a ' + typeof module
);
560 if ( typeof registry
[module
] !== 'undefined' ) {
561 throw new Error( 'module already implemeneted: ' + module
);
563 // List the module as registered
565 'state': 'registered',
566 'group': typeof group
=== 'string' ? group
: null,
568 'version': typeof version
!== 'undefined' ? parseInt( version
) : 0
570 if ( typeof dependencies
=== 'string' ) {
571 // Allow dependencies to be given as a single module name
572 registry
[module
].dependencies
= [dependencies
];
573 } else if ( typeof dependencies
=== 'object' || typeof dependencies
=== 'function' ) {
574 // Allow dependencies to be given as an array of module names or a function which returns an array
575 registry
[module
].dependencies
= dependencies
;
580 * Implements a module, giving the system a course of action to take upon loading. Results of a request for
581 * one or more modules contain calls to this function.
583 this.implement = function( module
, script
, style
, localization
) {
584 // Automaically register module
585 if ( typeof registry
[module
] === 'undefined' ) {
586 that
.register( module
);
589 if ( typeof script
!== 'function' ) {
590 throw new Error( 'script must be a function, not a ' + typeof script
);
592 if ( typeof style
!== 'undefined' && typeof style
!== 'string' && typeof style
!== 'object' ) {
593 throw new Error( 'style must be a string or object, not a ' + typeof style
);
595 if ( typeof localization
!== 'undefined' && typeof localization
!== 'object' ) {
596 throw new Error( 'localization must be an object, not a ' + typeof localization
);
598 if ( typeof registry
[module
] !== 'undefined' && typeof registry
[module
].script
!== 'undefined' ) {
599 throw new Error( 'module already implemeneted: ' + module
);
601 // Mark module as loaded
602 registry
[module
].state
= 'loaded';
604 registry
[module
].script
= script
;
605 if ( typeof style
=== 'string' || typeof style
=== 'object' && !( style
instanceof Array
) ) {
606 registry
[module
].style
= style
;
608 if ( typeof localization
=== 'object' ) {
609 registry
[module
].messages
= localization
;
611 // Execute or queue callback
612 if ( filter( ['ready'], registry
[module
].dependencies
).compare( registry
[module
].dependencies
) ) {
620 * Executes a function as soon as one or more required modules are ready
622 * @param mixed string or array of strings of modules names the callback dependencies to be ready before
624 * @param function callback to execute when all dependencies are ready (optional)
625 * @param function callback to execute when if dependencies have a errors (optional)
627 this.using = function( dependencies
, ready
, error
) {
629 if ( typeof dependencies
!== 'object' && typeof dependencies
!== 'string' ) {
630 throw new Error( 'dependencies must be a string or an array, not a ' + typeof dependencies
)
632 // Allow calling with a single dependency as a string
633 if ( typeof dependencies
=== 'string' ) {
634 dependencies
= [dependencies
];
636 // Resolve entire dependency map
637 dependencies
= resolve( dependencies
);
638 // If all dependencies are met, execute ready immediately
639 if ( filter( ['ready'], dependencies
).compare( dependencies
) ) {
640 if ( typeof ready
!== 'function' ) {
644 // If any dependencies have errors execute error immediately
645 else if ( filter( ['error'], dependencies
).length
) {
646 if ( typeof error
=== 'function' ) {
650 // Since some dependencies are not yet ready, queue up a request
652 request( dependencies
, ready
, error
);
657 * Loads an external script or one or more modules for future use
659 * @param {mixed} modules either the name of a module, array of modules, or a URL of an external script or style
660 * @param {string} type mime-type to use if calling with a URL of an external script or style; acceptable values
661 * are "text/css" and "text/javascript"; if no type is provided, text/javascript is assumed
663 this.load = function( modules
, type
) {
665 if ( typeof modules
!== 'object' && typeof modules
!== 'string' ) {
666 throw new Error( 'dependencies must be a string or an array, not a ' + typeof dependencies
)
668 // Allow calling with an external script or single dependency as a string
669 if ( typeof modules
=== 'string' ) {
670 // Support adding arbitrary external scripts
671 if ( modules
.substr( 0, 7 ) == 'http://' || modules
.substr( 0, 8 ) == 'https://' ) {
672 if ( type
=== 'text/css' ) {
673 setTimeout( function() {
674 $( 'head' ).append( '<link rel="stylesheet" type="text/css" href="' + modules
+ '" />' );
677 } else if ( type
=== 'text/javascript' || typeof type
=== 'undefined' ) {
678 setTimeout( function() {
679 $( 'body' ).append( '<script type="text/javascript" src="' + modules
+ '"></script>' );
686 // Called with single module
689 // Resolve entire dependency map
690 modules
= resolve( modules
);
691 // If all modules are ready, nothing dependency be done
692 if ( filter( ['ready'], modules
).compare( modules
) ) {
695 // If any modules have errors return false
696 else if ( filter( ['error'], modules
).length
) {
699 // Since some modules are not yet ready, queue up a request
707 * Flushes the request queue and begin executing load requests on demand
709 this.go = function() {
715 * Changes the state of a module
717 * @param mixed module string module name or object of module name/state pairs
718 * @param string state string state name
720 this.state = function( module
, state
) {
721 if ( typeof module
=== 'object' ) {
722 for ( var m
in module
) {
723 that
.state( m
, module
[m
] );
727 if ( module
in registry
) {
728 registry
[module
].state
= state
;
733 * Gets the version of a module
735 * @param string module name of module to get version for
737 this.version = function( module
) {
738 if ( module
in registry
&& 'version' in registry
[module
] ) {
739 return formatVersionNumber( registry
[module
].version
);
744 /* Cache document ready status */
746 $(document
).ready( function() { ready
= true; } );
749 /* Extension points */
757 /* Auto-register from pre-loaded startup scripts */
759 if ( typeof window
['startUp'] === 'function' ) {
761 delete window
['startUp'];