* Added exporting of user preferences
[lhc/web/wiklou.git] / resources / mediawiki / mediawiki.js
1 /*
2 * JavaScript backwards-compatibility and support
3 */
4
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 ) {
10 return i;
11 }
12 }
13 return -1;
14 };
15 }
16 // Add array comparison functionality
17 if ( typeof Array.prototype.compare === 'undefined' ) {
18 Array.prototype.compare = function( against ) {
19 if ( this.length != against.length ) {
20 return false;
21 }
22 for ( var i = 0; i < against.length; i++ ) {
23 if ( this[i].compare ) {
24 if ( !this[i].compare( against[i] ) ) {
25 return false;
26 }
27 }
28 if ( this[i] !== against[i] ) {
29 return false;
30 }
31 }
32 return true;
33 };
34 }
35
36 /*
37 * Core MediaWiki JavaScript Library
38 */
39 // Attach to window
40 window.mediaWiki = new ( function( $ ) {
41
42 /* Constants */
43
44 // This will not change until we are 100% ready to turn off legacy globals
45 var LEGACY_GLOBALS = true;
46
47 /* Private Members */
48
49 var that = this;
50
51 /* Prototypes */
52
53 this.prototypes = {
54 /*
55 * An object which allows single and multiple get/set/exists functionality on a list of key / value pairs
56 *
57 * @param {boolean} global whether to get/set/exists values on the window object or a private object
58 */
59 'configuration': function( global ) {
60
61 /* Private Members */
62
63 var that = this;
64 var values = global === true ? window : {};
65
66 /* Public Methods */
67
68 /**
69 * Gets one or multiple configuration values using a key and an optional fallback or an array of keys
70 */
71 this.get = function( keys, fallback ) {
72 if ( typeof keys === 'object' ) {
73 var result = {};
74 for ( var k = 0; k < keys.length; k++ ) {
75 if ( typeof values[keys[k]] !== 'undefined' ) {
76 result[keys[k]] = values[keys[k]];
77 }
78 }
79 return result;
80 } else if ( typeof keys === 'string' ) {
81 if ( typeof values[keys] === 'undefined' ) {
82 return typeof fallback !== 'undefined' ? fallback : null;
83 } else {
84 return values[keys];
85 }
86 } else {
87 return values;
88 }
89 };
90 /**
91 * Sets one or multiple configuration values using a key and a value or an object of keys and values
92 */
93 this.set = function( keys, value ) {
94 if ( typeof keys === 'object' ) {
95 for ( var k in keys ) {
96 values[k] = keys[k];
97 }
98 } else if ( typeof keys === 'string' && typeof value !== 'undefined' ) {
99 values[keys] = value;
100 }
101 };
102 /**
103 * Checks if one or multiple configuration fields exist
104 */
105 this.exists = function( keys ) {
106 if ( typeof keys === 'object' ) {
107 for ( var k = 0; k < keys.length; k++ ) {
108 if ( !( keys[k] in values ) ) {
109 return false;
110 }
111 }
112 return true;
113 } else {
114 return keys in values;
115 }
116 };
117 }
118 };
119
120 /* Methods */
121
122 /*
123 * Dummy function which in debug mode can be replaced with a function that does something clever
124 */
125 this.log = function() { };
126 /*
127 * List of configuration values
128 *
129 * In legacy mode the values this object wraps will be in the global space
130 */
131 this.config = new this.prototypes.configuration( LEGACY_GLOBALS );
132 /*
133 * Information about the current user
134 */
135 this.user = new ( function() {
136
137 /* Public Members */
138
139 this.options = new that.prototypes.configuration();
140 } )();
141 /*
142 * Localization system
143 */
144 this.msg = new ( function() {
145
146 /* Private Members */
147
148 var that = this;
149 // List of localized messages
150 var messages = {};
151
152 /* Public Methods */
153
154 this.set = function( keys, value ) {
155 if ( typeof keys === 'object' ) {
156 for ( var k in keys ) {
157 messages[k] = keys[k];
158 }
159 } else if ( typeof keys === 'string' && typeof value !== 'undefined' ) {
160 messages[keys] = value;
161 }
162 };
163 this.get = function( key, args ) {
164 if ( !( key in messages ) ) {
165 return '<' + key + '>';
166 }
167 var msg = messages[key];
168 if ( typeof args == 'object' || typeof args == 'array' ) {
169 for ( var a = 0; a < args.length; a++ ) {
170 msg = msg.replace( '\$' + ( parseInt( a ) + 1 ), args[a] );
171 }
172 } else if ( typeof args == 'string' || typeof args == 'number' ) {
173 msg = msg.replace( '$1', args );
174 }
175 return msg;
176 };
177 } )();
178 /*
179 * Client-side module loader which integrates with the MediaWiki ResourceLoader
180 */
181 this.loader = new ( function() {
182
183 /* Private Members */
184
185 var that = this;
186 /*
187 * Mapping of registered modules
188 *
189 * The jquery module is pre-registered, because it must have already been provided for this object to have
190 * been built, and in debug mode jquery would have been provided through a unique loader request, making it
191 * impossible to hold back registration of jquery until after mediawiki.
192 *
193 * Format:
194 * {
195 * 'moduleName': {
196 * 'dependencies': ['required module', 'required module', ...], (or) function() {}
197 * 'state': 'registered', 'loading', 'loaded', 'ready', or 'error'
198 * 'script': function() {},
199 * 'style': 'css code string',
200 * 'messages': { 'key': 'value' },
201 * 'version': ############## (unix timestamp)
202 * }
203 * }
204 */
205 var registry = {};
206 // List of modules which will be loaded as when ready
207 var batch = [];
208 // List of modules to be loaded
209 var queue = [];
210 // List of callback functions waiting for modules to be ready to be called
211 var jobs = [];
212 // Flag indicating that requests should be suspended
213 var suspended = true;
214 // Flag inidicating that document ready has occured
215 var ready = false;
216
217 /* Private Methods */
218
219 /**
220 * Generates an ISO8601 string from a UNIX timestamp
221 */
222 function formatVersionNumber( timestamp ) {
223 var date = new Date();
224 date.setTime( timestamp * 1000 );
225 function pad1( n ) {
226 return n < 10 ? '0' + n : n
227 }
228 function pad2( n ) {
229 return n < 10 ? '00' + n : ( n < 100 ? '0' + n : n );
230 }
231 return date.getUTCFullYear() + '-' +
232 pad1( date.getUTCMonth() + 1 ) + '-' +
233 pad1( date.getUTCDate() ) + 'T' +
234 pad1( date.getUTCHours() ) + ':' +
235 pad1( date.getUTCMinutes() ) + ':' +
236 pad1( date.getUTCSeconds() ) +
237 'Z';
238 }
239 /**
240 * Recursively resolves dependencies and detects circular references
241 */
242 function recurse( module, resolved, unresolved ) {
243 unresolved[unresolved.length] = module;
244 // Resolves dynamic loader function and replaces it with it's own results
245 if ( typeof registry[module].dependencies === 'function' ) {
246 registry[module].dependencies = registry[module].dependencies();
247 // Gaurantees the module's dependencies are always in an array
248 if ( typeof registry[module].dependencies !== 'object' ) {
249 registry[module].dependencies = [registry[module].dependencies];
250 }
251 }
252 // Tracks down dependencies
253 for ( var n = 0; n < registry[module].dependencies.length; n++ ) {
254 if ( resolved.indexOf( registry[module].dependencies[n] ) === -1 ) {
255 if ( unresolved.indexOf( registry[module].dependencies[n] ) !== -1 ) {
256 throw new Error(
257 'Circular reference detected: ' + module + ' -> ' + registry[module].dependencies[n]
258 );
259 }
260 recurse( registry[module].dependencies[n], resolved, unresolved );
261 }
262 }
263 resolved[resolved.length] = module;
264 unresolved.splice( unresolved.indexOf( module ), 1 );
265 }
266 /**
267 * Gets a list of modules names that a module dependencies in their proper dependency order
268 *
269 * @param mixed string module name or array of string module names
270 * @return list of dependencies
271 * @throws Error if circular reference is detected
272 */
273 function resolve( module, resolved, unresolved ) {
274 // Allow calling with an array of module names
275 if ( typeof module === 'object' ) {
276 var modules = [];
277 for ( var m = 0; m < module.length; m++ ) {
278 var dependencies = resolve( module[m] );
279 for ( var n = 0; n < dependencies.length; n++ ) {
280 modules[modules.length] = dependencies[n];
281 }
282 }
283 return modules;
284 } else if ( typeof module === 'string' ) {
285 // Undefined modules have no dependencies
286 if ( !( module in registry ) ) {
287 return [];
288 }
289 var resolved = [];
290 recurse( module, resolved, [] );
291 return resolved;
292 }
293 throw new Error( 'Invalid module argument: ' + module );
294 };
295 /**
296 * Narrows a list of module names down to those matching a specific state. Possible states are 'undefined',
297 * 'registered', 'loading', 'loaded', or 'ready'
298 *
299 * @param mixed string or array of strings of module states to filter by
300 * @param array list of module names to filter (optional, all modules will be used by default)
301 * @return array list of filtered module names
302 */
303 function filter( states, modules ) {
304 // Allow states to be given as a string
305 if ( typeof states === 'string' ) {
306 states = [states];
307 }
308 // If called without a list of modules, build and use a list of all modules
309 var list = [];
310 if ( typeof modules === 'undefined' ) {
311 modules = [];
312 for ( module in registry ) {
313 modules[modules.length] = module;
314 }
315 }
316 // Build a list of modules which are in one of the specified states
317 for ( var s = 0; s < states.length; s++ ) {
318 for ( var m = 0; m < modules.length; m++ ) {
319 if (
320 ( states[s] == 'undefined' && typeof registry[modules[m]] === 'undefined' ) ||
321 ( typeof registry[modules[m]] === 'object' && registry[modules[m]].state === states[s] )
322 ) {
323 list[list.length] = modules[m];
324 }
325 }
326 }
327 return list;
328 }
329 /**
330 * Executes a loaded module, making it ready to use
331 *
332 * @param string module name to execute
333 */
334 function execute( module ) {
335 if ( typeof registry[module] === 'undefined' ) {
336 throw new Error( 'Module has not been registered yet: ' + module );
337 } else if ( registry[module].state === 'registered' ) {
338 throw new Error( 'Module has not been requested from the server yet: ' + module );
339 } else if ( registry[module].state === 'loading' ) {
340 throw new Error( 'Module has not completed loading yet: ' + module );
341 } else if ( registry[module].state === 'ready' ) {
342 throw new Error( 'Module has already been loaded: ' + module );
343 }
344 // Add style sheet to document
345 if ( typeof registry[module].style === 'string' && registry[module].style.length ) {
346 $( 'head' ).append( '<style type="text/css">' + registry[module].style + '</style>' );
347 } else if ( typeof registry[module].style === 'object' ) {
348 for ( var media in registry[module].style ) {
349 $( 'head' ).append(
350 '<style type="text/css" media="' + media + '">' + registry[module].style[media] + '</style>'
351 );
352 }
353 }
354 // Add localizations to message system
355 if ( typeof registry[module].messages === 'object' ) {
356 mediaWiki.msg.set( registry[module].messages );
357 }
358 // Execute script
359 try {
360 registry[module].script();
361 registry[module].state = 'ready';
362 // Run jobs who's dependencies have just been met
363 for ( var j = 0; j < jobs.length; j++ ) {
364 if ( filter( 'ready', jobs[j].dependencies ).compare( jobs[j].dependencies ) ) {
365 if ( typeof jobs[j].ready === 'function' ) {
366 jobs[j].ready();
367 }
368 jobs.splice( j, 1 );
369 j--;
370 }
371 }
372 // Execute modules who's dependencies have just been met
373 for ( r in registry ) {
374 if ( registry[r].state == 'loaded' ) {
375 if ( filter( ['ready'], registry[r].dependencies ).compare( registry[r].dependencies ) ) {
376 execute( r );
377 }
378 }
379 }
380 } catch ( e ) {
381 mediaWiki.log( 'Exception thrown by ' + module + ': ' + e.message );
382 mediaWiki.log( e );
383 registry[module].state = 'error';
384 // Run error callbacks of jobs affected by this condition
385 for ( var j = 0; j < jobs.length; j++ ) {
386 if ( jobs[j].dependencies.indexOf( module ) !== -1 ) {
387 if ( typeof jobs[j].error === 'function' ) {
388 jobs[j].error();
389 }
390 jobs.splice( j, 1 );
391 j--;
392 }
393 }
394 }
395 }
396 /**
397 * Adds a dependencies to the queue with optional callbacks to be run when the dependencies are ready or fail
398 *
399 * @param mixed string moulde name or array of string module names
400 * @param function ready callback to execute when all dependencies are ready
401 * @param function error callback to execute when any dependency fails
402 */
403 function request( dependencies, ready, error ) {
404 // Allow calling by single module name
405 if ( typeof dependencies === 'string' ) {
406 dependencies = [dependencies];
407 if ( dependencies[0] in registry ) {
408 for ( var n = 0; n < registry[dependencies[0]].dependencies.length; n++ ) {
409 dependencies[dependencies.length] = registry[dependencies[0]].dependencies[n];
410 }
411 }
412 }
413 // Add ready and error callbacks if they were given
414 if ( arguments.length > 1 ) {
415 jobs[jobs.length] = {
416 'dependencies': filter( ['undefined', 'registered', 'loading', 'loaded'], dependencies ),
417 'ready': ready,
418 'error': error
419 };
420 }
421 // Queue up any dependencies that are undefined or registered
422 dependencies = filter( ['undefined', 'registered'], dependencies );
423 for ( var n = 0; n < dependencies.length; n++ ) {
424 if ( queue.indexOf( dependencies[n] ) === -1 ) {
425 queue[queue.length] = dependencies[n];
426 }
427 }
428 // Work the queue
429 that.work();
430 }
431
432 function sortQuery(o) {
433 var sorted = {}, key, a = [];
434 for ( key in o ) {
435 if ( o.hasOwnProperty( key ) ) {
436 a.push( key );
437 }
438 }
439 a.sort();
440 for ( key = 0; key < a.length; key++ ) {
441 sorted[a[key]] = o[a[key]];
442 }
443 return sorted;
444 }
445
446 /* Public Methods */
447
448 /**
449 * Requests dependencies from server, loading and executing when things when ready.
450 */
451 this.work = function() {
452 // Appends a list of modules to the batch
453 for ( var q = 0; q < queue.length; q++ ) {
454 // Only request modules which are undefined or registered
455 if ( !( queue[q] in registry ) || registry[queue[q]].state == 'registered' ) {
456 // Prevent duplicate entries
457 if ( batch.indexOf( queue[q] ) === -1 ) {
458 batch[batch.length] = queue[q];
459 // Mark registered modules as loading
460 if ( queue[q] in registry ) {
461 registry[queue[q]].state = 'loading';
462 }
463 }
464 }
465 }
466 // Clean up the queue
467 queue = [];
468 // After document ready, handle the batch
469 if ( !suspended && batch.length ) {
470 // Always order modules alphabetically to help reduce cache misses for otherwise identical content
471 batch.sort();
472 // Build a list of request parameters
473 var base = {
474 'skin': mediaWiki.config.get( 'skin' ),
475 'lang': mediaWiki.config.get( 'wgUserLanguage' ),
476 'debug': mediaWiki.config.get( 'debug' )
477 };
478 // Extend request parameters with a list of modules in the batch
479 var requests = [];
480 if ( base.debug == '1' ) {
481 for ( var b = 0; b < batch.length; b++ ) {
482 requests[requests.length] = $.extend(
483 { 'modules': batch[b], 'version': registry[batch[b]].version }, base
484 );
485 }
486 } else {
487 // Calculate the highest timestamp
488 var version = 0;
489 for ( var b = 0; b < batch.length; b++ ) {
490 if ( registry[batch[b]].version > version ) {
491 version = registry[batch[b]].version;
492 }
493 }
494 requests[requests.length] = $.extend(
495 { 'modules': batch.join( '|' ), 'version': formatVersionNumber( version ) }, base
496 );
497 }
498 // Clear the batch - this MUST happen before we append the script element to the body or it's
499 // possible that the script will be locally cached, instantly load, and work the batch again,
500 // all before we've cleared it causing each request to include modules which are already loaded
501 batch = [];
502 // Asynchronously append a script tag to the end of the body
503 function request() {
504 var html = '';
505 for ( var r = 0; r < requests.length; r++ ) {
506 requests[r] = sortQuery( requests[r] );
507 // Build out the HTML
508 var src = mediaWiki.config.get( 'wgLoadScript' ) + '?' + $.param( requests[r] );
509 html += '<script type="text/javascript" src="' + src + '"></script>';
510 }
511 return html;
512 }
513 // Load asynchronously after doumument ready
514 if ( ready ) {
515 setTimeout( function() { $( 'body' ).append( request() ); }, 0 )
516 } else {
517 document.write( request() );
518 }
519 }
520 };
521 /**
522 * Registers a module, letting the system know about it and it's dependencies. loader.js files contain calls
523 * to this function.
524 */
525 this.register = function( module, version, dependencies, status ) {
526 // Allow multiple registration
527 if ( typeof module === 'object' ) {
528 for ( var m = 0; m < module.length; m++ ) {
529 if ( typeof module[m] === 'string' ) {
530 that.register( module[m] );
531 } else if ( typeof module[m] === 'object' ) {
532 that.register.apply( that, module[m] );
533 }
534 }
535 return;
536 }
537 // Validate input
538 if ( typeof module !== 'string' ) {
539 throw new Error( 'module must be a string, not a ' + typeof module );
540 }
541 if ( typeof registry[module] !== 'undefined' && typeof status === 'undefined' ) {
542 throw new Error( 'module already implemeneted: ' + module );
543 }
544 // List the module as registered
545 registry[module] = {
546 'state': typeof status === 'string' ? status : 'registered',
547 'dependencies': [],
548 'version': typeof version !== 'undefined' ? parseInt( version ) : 0
549 };
550 if ( typeof dependencies === 'string' ) {
551 // Allow dependencies to be given as a single module name
552 registry[module].dependencies = [dependencies];
553 } else if ( typeof dependencies === 'object' || typeof dependencies === 'function' ) {
554 // Allow dependencies to be given as an array of module names or a function which returns an array
555 registry[module].dependencies = dependencies;
556 }
557 };
558 /**
559 * Implements a module, giving the system a course of action to take upon loading. Results of a request for
560 * one or more modules contain calls to this function.
561 */
562 this.implement = function( module, script, style, localization ) {
563 // Automaically register module
564 if ( typeof registry[module] === 'undefined' ) {
565 that.register( module );
566 }
567 // Validate input
568 if ( typeof script !== 'function' ) {
569 throw new Error( 'script must be a function, not a ' + typeof script );
570 }
571 if ( typeof style !== 'undefined' && typeof style !== 'string' && typeof style !== 'object' ) {
572 throw new Error( 'style must be a string or object, not a ' + typeof style );
573 }
574 if ( typeof localization !== 'undefined' && typeof localization !== 'object' ) {
575 throw new Error( 'localization must be an object, not a ' + typeof localization );
576 }
577 if ( typeof registry[module] !== 'undefined' && typeof registry[module].script !== 'undefined' ) {
578 throw new Error( 'module already implemeneted: ' + module );
579 }
580 // Mark module as loaded
581 registry[module].state = 'loaded';
582 // Attach components
583 registry[module].script = script;
584 if ( typeof style === 'string' || typeof style === 'object' ) {
585 registry[module].style = style;
586 }
587 if ( typeof localization === 'object' ) {
588 registry[module].messages = localization;
589 }
590 // Execute or queue callback
591 if ( filter( ['ready'], registry[module].dependencies ).compare( registry[module].dependencies ) ) {
592 execute( module );
593 } else {
594 request( module );
595 }
596 };
597 /**
598 * Executes a function as soon as one or more required modules are ready
599 *
600 * @param mixed string or array of strings of modules names the callback dependencies to be ready before
601 * executing
602 * @param function callback to execute when all dependencies are ready (optional)
603 * @param function callback to execute when if dependencies have a errors (optional)
604 */
605 this.using = function( dependencies, ready, error ) {
606 // Validate input
607 if ( typeof dependencies !== 'object' && typeof dependencies !== 'string' ) {
608 throw new Error( 'dependencies must be a string or an array, not a ' + typeof dependencies )
609 }
610 // Allow calling with a single dependency as a string
611 if ( typeof dependencies === 'string' ) {
612 dependencies = [dependencies];
613 }
614 // Resolve entire dependency map
615 dependencies = resolve( dependencies );
616 // If all dependencies are met, execute ready immediately
617 if ( filter( ['ready'], dependencies ).compare( dependencies ) ) {
618 if ( typeof ready !== 'function' ) {
619 ready();
620 }
621 }
622 // If any dependencies have errors execute error immediately
623 else if ( filter( ['error'], dependencies ).length ) {
624 if ( typeof error === 'function' ) {
625 error();
626 }
627 }
628 // Since some dependencies are not yet ready, queue up a request
629 else {
630 request( dependencies, ready, error );
631 }
632 };
633 /**
634 * Loads one or more modules for future use
635 */
636 this.load = function( modules ) {
637 // Validate input
638 if ( typeof modules !== 'object' && typeof modules !== 'string' ) {
639 throw new Error( 'dependencies must be a string or an array, not a ' + typeof dependencies )
640 }
641 // Allow calling with a single dependency as a string
642 if ( typeof modules === 'string' ) {
643 modules = [modules];
644 }
645 // Resolve entire dependency map
646 modules = resolve( modules );
647 // If all modules are ready, nothing dependency be done
648 if ( filter( ['ready'], modules ).compare( modules ) ) {
649 return true;
650 }
651 // If any modules have errors return false
652 else if ( filter( ['error'], modules ).length ) {
653 return false;
654 }
655 // Since some modules are not yet ready, queue up a request
656 else {
657 request( modules );
658 return true;
659 }
660 };
661 /**
662 * Flushes the request queue and begin executing load requests on demand
663 */
664 this.go = function() {
665 suspended = false;
666 that.work();
667 };
668 /**
669 * Changes the state of a module
670 *
671 * @param mixed module string module name or object of module name/state pairs
672 * @param string state string state name
673 */
674 this.state = function( module, state ) {
675 if ( typeof module === 'object' ) {
676 for ( var m in module ) {
677 that.state( m, module[m] );
678 }
679 return;
680 }
681 if ( module in registry ) {
682 registry[module].state = state;
683 }
684 };
685
686 /* Cache document ready status */
687
688 $(document).ready( function() { ready = true; } );
689 } )();
690
691 /* Extension points */
692
693 this.util = {};
694 this.legacy = {};
695
696 } )( jQuery );
697
698
699 /* Auto-register from pre-loaded startup scripts */
700
701 if ( typeof window['startUp'] === 'function' ) {
702 window['startUp']();
703 delete window['startUp'];
704 }