2fcb5f758f1dc055bd717b1746b4c50ec23e260b
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 "basic" string from a UNIX timestamp
235 function formatVersionNumber( timestamp
) {
236 function pad( a
, b
, c
) {
237 return [a
< 10 ? '0' + a
: a
, b
< 10 ? '0' + b
: b
, c
< 10 ? '0' + c
: c
].join();
240 d
.setTime( timestamp
* 1000 );
242 pad( d
.getUTCFullYear(), d
.getUTCMonth() + 1, d
.getUTCDate() ), 'T',
243 pad( d
.getUTCHours(), d
.getUTCMinutes(), d
.getUTCSeconds() ), 'Z'
248 * Recursively resolves dependencies and detects circular references
250 function recurse( module
, resolved
, unresolved
) {
251 unresolved
[unresolved
.length
] = module
;
252 // Resolves dynamic loader function and replaces it with it's own results
253 if ( typeof registry
[module
].dependencies
=== 'function' ) {
254 registry
[module
].dependencies
= registry
[module
].dependencies();
255 // Gaurantees the module's dependencies are always in an array
256 if ( typeof registry
[module
].dependencies
!== 'object' ) {
257 registry
[module
].dependencies
= [registry
[module
].dependencies
];
260 // Tracks down dependencies
261 for ( var n
= 0; n
< registry
[module
].dependencies
.length
; n
++ ) {
262 if ( resolved
.indexOf( registry
[module
].dependencies
[n
] ) === -1 ) {
263 if ( unresolved
.indexOf( registry
[module
].dependencies
[n
] ) !== -1 ) {
265 'Circular reference detected: ' + module
+ ' -> ' + registry
[module
].dependencies
[n
]
268 recurse( registry
[module
].dependencies
[n
], resolved
, unresolved
);
271 resolved
[resolved
.length
] = module
;
272 unresolved
.splice( unresolved
.indexOf( module
), 1 );
276 * Gets a list of modules names that a module dependencies in their proper dependency order
278 * @param mixed string module name or array of string module names
279 * @return list of dependencies
280 * @throws Error if circular reference is detected
282 function resolve( module
, resolved
, unresolved
) {
283 // Allow calling with an array of module names
284 if ( typeof module
=== 'object' ) {
286 for ( var m
= 0; m
< module
.length
; m
++ ) {
287 var dependencies
= resolve( module
[m
] );
288 for ( var n
= 0; n
< dependencies
.length
; n
++ ) {
289 modules
[modules
.length
] = dependencies
[n
];
293 } else if ( typeof module
=== 'string' ) {
294 // Undefined modules have no dependencies
295 if ( !( module
in registry
) ) {
299 recurse( module
, resolved
, [] );
302 throw new Error( 'Invalid module argument: ' + module
);
306 * Narrows a list of module names down to those matching a specific state. Possible states are 'undefined',
307 * 'registered', 'loading', 'loaded', or 'ready'
309 * @param mixed string or array of strings of module states to filter by
310 * @param array list of module names to filter (optional, all modules will be used by default)
311 * @return array list of filtered module names
313 function filter( states
, modules
) {
314 // Allow states to be given as a string
315 if ( typeof states
=== 'string' ) {
318 // If called without a list of modules, build and use a list of all modules
320 if ( typeof modules
=== 'undefined' ) {
322 for ( module
in registry
) {
323 modules
[modules
.length
] = module
;
326 // Build a list of modules which are in one of the specified states
327 for ( var s
= 0; s
< states
.length
; s
++ ) {
328 for ( var m
= 0; m
< modules
.length
; m
++ ) {
330 ( states
[s
] == 'undefined' && typeof registry
[modules
[m
]] === 'undefined' ) ||
331 ( typeof registry
[modules
[m
]] === 'object' && registry
[modules
[m
]].state
=== states
[s
] )
333 list
[list
.length
] = modules
[m
];
341 * Executes a loaded module, making it ready to use
343 * @param string module name to execute
345 function execute( module
) {
346 if ( typeof registry
[module
] === 'undefined' ) {
347 throw new Error( 'Module has not been registered yet: ' + module
);
348 } else if ( registry
[module
].state
=== 'registered' ) {
349 throw new Error( 'Module has not been requested from the server yet: ' + module
);
350 } else if ( registry
[module
].state
=== 'loading' ) {
351 throw new Error( 'Module has not completed loading yet: ' + module
);
352 } else if ( registry
[module
].state
=== 'ready' ) {
353 throw new Error( 'Module has already been loaded: ' + module
);
355 // Add style sheet to document
356 if ( typeof registry
[module
].style
=== 'string' && registry
[module
].style
.length
) {
357 $( 'head' ).append( '<style type="text/css">' + registry
[module
].style
+ '</style>' );
358 } else if ( typeof registry
[module
].style
=== 'object' && !( registry
[module
].style
instanceof Array
) ) {
359 for ( var media
in registry
[module
].style
) {
361 '<style type="text/css" media="' + media
+ '">' + registry
[module
].style
[media
] + '</style>'
365 // Add localizations to message system
366 if ( typeof registry
[module
].messages
=== 'object' ) {
367 mediaWiki
.msg
.set( registry
[module
].messages
);
371 registry
[module
].script();
372 registry
[module
].state
= 'ready';
373 // Run jobs who's dependencies have just been met
374 for ( var j
= 0; j
< jobs
.length
; j
++ ) {
375 if ( filter( 'ready', jobs
[j
].dependencies
).compare( jobs
[j
].dependencies
) ) {
376 if ( typeof jobs
[j
].ready
=== 'function' ) {
383 // Execute modules who's dependencies have just been met
384 for ( r
in registry
) {
385 if ( registry
[r
].state
== 'loaded' ) {
386 if ( filter( ['ready'], registry
[r
].dependencies
).compare( registry
[r
].dependencies
) ) {
392 mediaWiki
.log( 'Exception thrown by ' + module
+ ': ' + e
.message
);
394 registry
[module
].state
= 'error';
395 // Run error callbacks of jobs affected by this condition
396 for ( var j
= 0; j
< jobs
.length
; j
++ ) {
397 if ( jobs
[j
].dependencies
.indexOf( module
) !== -1 ) {
398 if ( typeof jobs
[j
].error
=== 'function' ) {
409 * Adds a dependencies to the queue with optional callbacks to be run when the dependencies are ready or fail
411 * @param mixed string moulde name or array of string module names
412 * @param function ready callback to execute when all dependencies are ready
413 * @param function error callback to execute when any dependency fails
415 function request( dependencies
, ready
, error
) {
416 // Allow calling by single module name
417 if ( typeof dependencies
=== 'string' ) {
418 dependencies
= [dependencies
];
419 if ( dependencies
[0] in registry
) {
420 for ( var n
= 0; n
< registry
[dependencies
[0]].dependencies
.length
; n
++ ) {
421 dependencies
[dependencies
.length
] = registry
[dependencies
[0]].dependencies
[n
];
425 // Add ready and error callbacks if they were given
426 if ( arguments
.length
> 1 ) {
427 jobs
[jobs
.length
] = {
428 'dependencies': filter( ['undefined', 'registered', 'loading', 'loaded'], dependencies
),
433 // Queue up any dependencies that are undefined or registered
434 dependencies
= filter( ['undefined', 'registered'], dependencies
);
435 for ( var n
= 0; n
< dependencies
.length
; n
++ ) {
436 if ( queue
.indexOf( dependencies
[n
] ) === -1 ) {
437 queue
[queue
.length
] = dependencies
[n
];
444 function sortQuery(o
) {
445 var sorted
= {}, key
, a
= [];
447 if ( o
.hasOwnProperty( key
) ) {
452 for ( key
= 0; key
< a
.length
; key
++ ) {
453 sorted
[a
[key
]] = o
[a
[key
]];
461 * Requests dependencies from server, loading and executing when things when ready.
463 this.work = function() {
464 // Appends a list of modules to the batch
465 for ( var q
= 0; q
< queue
.length
; q
++ ) {
466 // Only request modules which are undefined or registered
467 if ( !( queue
[q
] in registry
) || registry
[queue
[q
]].state
== 'registered' ) {
468 // Prevent duplicate entries
469 if ( batch
.indexOf( queue
[q
] ) === -1 ) {
470 batch
[batch
.length
] = queue
[q
];
471 // Mark registered modules as loading
472 if ( queue
[q
] in registry
) {
473 registry
[queue
[q
]].state
= 'loading';
478 // Clean up the queue
480 // After document ready, handle the batch
481 if ( !suspended
&& batch
.length
) {
482 // Always order modules alphabetically to help reduce cache misses for otherwise identical content
484 // Build a list of request parameters
486 'skin': mediaWiki
.config
.get( 'skin' ),
487 'lang': mediaWiki
.config
.get( 'wgUserLanguage' ),
488 'debug': mediaWiki
.config
.get( 'debug' )
490 // Extend request parameters with a list of modules in the batch
492 if ( base
.debug
== '1' ) {
493 for ( var b
= 0; b
< batch
.length
; b
++ ) {
494 requests
[requests
.length
] = $.extend(
495 { 'modules': batch
[b
], 'version': registry
[batch
[b
]].version
}, base
501 for ( var b
= 0; b
< batch
.length
; b
++ ) {
502 var group
= registry
[batch
[b
]].group
;
503 if ( !( group
in groups
) ) {
506 groups
[group
][groups
[group
].length
] = batch
[b
];
508 for ( var group
in groups
) {
509 // Calculate the highest timestamp
511 for ( var g
= 0; g
< groups
[group
].length
; g
++ ) {
512 if ( registry
[groups
[group
][g
]].version
> version
) {
513 version
= registry
[groups
[group
][g
]].version
;
516 requests
[requests
.length
] = $.extend(
517 { 'modules': groups
[group
].join( '|' ), 'version': formatVersionNumber( version
) }, base
521 // Clear the batch - this MUST happen before we append the script element to the body or it's
522 // possible that the script will be locally cached, instantly load, and work the batch again,
523 // all before we've cleared it causing each request to include modules which are already loaded
525 // Asynchronously append a script tag to the end of the body
528 for ( var r
= 0; r
< requests
.length
; r
++ ) {
529 requests
[r
] = sortQuery( requests
[r
] );
530 // Build out the HTML
531 var src
= mediaWiki
.config
.get( 'wgLoadScript' ) + '?' + $.param( requests
[r
] );
532 html
+= '<script type="text/javascript" src="' + src
+ '"></script>';
536 // Load asynchronously after doumument ready
538 setTimeout( function() { $( 'body' ).append( request() ); }, 0 )
540 document
.write( request() );
546 * Registers a module, letting the system know about it and it's dependencies. loader.js files contain calls
549 this.register = function( module
, version
, dependencies
, group
) {
550 // Allow multiple registration
551 if ( typeof module
=== 'object' ) {
552 for ( var m
= 0; m
< module
.length
; m
++ ) {
553 if ( typeof module
[m
] === 'string' ) {
554 that
.register( module
[m
] );
555 } else if ( typeof module
[m
] === 'object' ) {
556 that
.register
.apply( that
, module
[m
] );
562 if ( typeof module
!== 'string' ) {
563 throw new Error( 'module must be a string, not a ' + typeof module
);
565 if ( typeof registry
[module
] !== 'undefined' ) {
566 throw new Error( 'module already implemeneted: ' + module
);
568 // List the module as registered
570 'state': 'registered',
571 'group': typeof group
=== 'string' ? group
: null,
573 'version': typeof version
!== 'undefined' ? parseInt( version
) : 0
575 if ( typeof dependencies
=== 'string' ) {
576 // Allow dependencies to be given as a single module name
577 registry
[module
].dependencies
= [dependencies
];
578 } else if ( typeof dependencies
=== 'object' || typeof dependencies
=== 'function' ) {
579 // Allow dependencies to be given as an array of module names or a function which returns an array
580 registry
[module
].dependencies
= dependencies
;
585 * Implements a module, giving the system a course of action to take upon loading. Results of a request for
586 * one or more modules contain calls to this function.
588 this.implement = function( module
, script
, style
, localization
) {
589 // Automaically register module
590 if ( typeof registry
[module
] === 'undefined' ) {
591 that
.register( module
);
594 if ( typeof script
!== 'function' ) {
595 throw new Error( 'script must be a function, not a ' + typeof script
);
597 if ( typeof style
!== 'undefined' && typeof style
!== 'string' && typeof style
!== 'object' ) {
598 throw new Error( 'style must be a string or object, not a ' + typeof style
);
600 if ( typeof localization
!== 'undefined' && typeof localization
!== 'object' ) {
601 throw new Error( 'localization must be an object, not a ' + typeof localization
);
603 if ( typeof registry
[module
] !== 'undefined' && typeof registry
[module
].script
!== 'undefined' ) {
604 throw new Error( 'module already implemeneted: ' + module
);
606 // Mark module as loaded
607 registry
[module
].state
= 'loaded';
609 registry
[module
].script
= script
;
610 if ( typeof style
=== 'string' || typeof style
=== 'object' && !( style
instanceof Array
) ) {
611 registry
[module
].style
= style
;
613 if ( typeof localization
=== 'object' ) {
614 registry
[module
].messages
= localization
;
616 // Execute or queue callback
617 if ( filter( ['ready'], registry
[module
].dependencies
).compare( registry
[module
].dependencies
) ) {
625 * Executes a function as soon as one or more required modules are ready
627 * @param mixed string or array of strings of modules names the callback dependencies to be ready before
629 * @param function callback to execute when all dependencies are ready (optional)
630 * @param function callback to execute when if dependencies have a errors (optional)
632 this.using = function( dependencies
, ready
, error
) {
634 if ( typeof dependencies
!== 'object' && typeof dependencies
!== 'string' ) {
635 throw new Error( 'dependencies must be a string or an array, not a ' + typeof dependencies
)
637 // Allow calling with a single dependency as a string
638 if ( typeof dependencies
=== 'string' ) {
639 dependencies
= [dependencies
];
641 // Resolve entire dependency map
642 dependencies
= resolve( dependencies
);
643 // If all dependencies are met, execute ready immediately
644 if ( filter( ['ready'], dependencies
).compare( dependencies
) ) {
645 if ( typeof ready
=== 'function' ) {
649 // If any dependencies have errors execute error immediately
650 else if ( filter( ['error'], dependencies
).length
) {
651 if ( typeof error
=== 'function' ) {
655 // Since some dependencies are not yet ready, queue up a request
657 request( dependencies
, ready
, error
);
662 * Loads an external script or one or more modules for future use
664 * @param {mixed} modules either the name of a module, array of modules, or a URL of an external script or style
665 * @param {string} type mime-type to use if calling with a URL of an external script or style; acceptable values
666 * are "text/css" and "text/javascript"; if no type is provided, text/javascript is assumed
668 this.load = function( modules
, type
) {
670 if ( typeof modules
!== 'object' && typeof modules
!== 'string' ) {
671 throw new Error( 'dependencies must be a string or an array, not a ' + typeof dependencies
)
673 // Allow calling with an external script or single dependency as a string
674 if ( typeof modules
=== 'string' ) {
675 // Support adding arbitrary external scripts
676 if ( modules
.substr( 0, 7 ) == 'http://' || modules
.substr( 0, 8 ) == 'https://' ) {
677 if ( type
=== 'text/css' ) {
678 setTimeout( function() {
679 $( 'head' ).append( '<link rel="stylesheet" type="text/css" href="' + modules
+ '" />' );
682 } else if ( type
=== 'text/javascript' || typeof type
=== 'undefined' ) {
683 setTimeout( function() {
684 $( 'body' ).append( '<script type="text/javascript" src="' + modules
+ '"></script>' );
691 // Called with single module
694 // Resolve entire dependency map
695 modules
= resolve( modules
);
696 // If all modules are ready, nothing dependency be done
697 if ( filter( ['ready'], modules
).compare( modules
) ) {
700 // If any modules have errors return false
701 else if ( filter( ['error'], modules
).length
) {
704 // Since some modules are not yet ready, queue up a request
712 * Flushes the request queue and begin executing load requests on demand
714 this.go = function() {
720 * Changes the state of a module
722 * @param mixed module string module name or object of module name/state pairs
723 * @param string state string state name
725 this.state = function( module
, state
) {
726 if ( typeof module
=== 'object' ) {
727 for ( var m
in module
) {
728 that
.state( m
, module
[m
] );
732 if ( !( module
in registry
) ) {
733 that
.register( module
);
735 registry
[module
].state
= state
;
739 * Gets the version of a module
741 * @param string module name of module to get version for
743 this.version = function( module
) {
744 if ( module
in registry
&& 'version' in registry
[module
] ) {
745 return formatVersionNumber( registry
[module
].version
);
750 /* Cache document ready status */
752 $(document
).ready( function() { ready
= true; } );
755 /* Extension points */
763 /* Auto-register from pre-loaded startup scripts */
765 if ( typeof window
['startUp'] === 'function' ) {
767 delete window
['startUp'];