Merging resourceloader branch into trunk. Full documentation is at http://www.mediawi...
[lhc/web/wiklou.git] / resources / mw / mw.js
1 /*
2 * JavaScript Backwards Compatibility
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
17 /*
18 * Core MediaWiki JavaScript Library
19 */
20 ( function() {
21
22 /* Constants */
23
24 // This will not change until we are 100% ready to turn off legacy globals
25 const LEGACY_GLOBALS = true;
26
27 /* Members */
28
29 this.legacy = LEGACY_GLOBALS ? window : {};
30
31 /* Methods */
32
33 /**
34 * Log a string msg to the console
35 *
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
41 *
42 * @author Michael Dale <mdale@wikimedia.org>, Trevor Parscal <tparscal@wikimedia.org>
43 * @param {String} string String to output to console
44 */
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;
49 }
50 // Try to use an existing console
51 if ( typeof window.console !== 'undefined' && typeof window.console.log == 'function' ) {
52 window.console.log( string );
53 } else {
54 // Show a log box for console-less browsers
55 var $log = $( '#mw_log_console' );
56 if ( !$log.length ) {
57 $log = $( '<div id="mw_log_console"></div>' )
58 .css( {
59 'position': 'absolute',
60 'overflow': 'auto',
61 'z-index': 500,
62 'bottom': '0px',
63 'left': '0px',
64 'right': '0px',
65 'height': '150px',
66 'background-color': 'white',
67 'border-top': 'solid 1px #DDDDDD'
68 } )
69 .appendTo( $( 'body' ) );
70 }
71 if ( $log.length ) {
72 $log.append(
73 $( '<div>' + string + '</div>' )
74 .css( {
75 'border-bottom': 'solid 1px #DDDDDD',
76 'font-size': 'small',
77 'font-family': 'monospace',
78 'padding': '0.125em 0.25em'
79 } )
80 );
81 }
82 }
83 };
84 /*
85 * An object which allows single and multiple existence, setting and getting on a list of key / value pairs
86 */
87 this.config = new ( function() {
88
89 /* Private Members */
90
91 var that = this;
92 // List of configuration values - in legacy mode these configurations were ALL in the global space
93 var values = LEGACY_GLOBALS ? window : {};
94
95 /* Public Methods */
96
97 /**
98 * Sets one or multiple configuration values using a key and a value or an object of keys and values
99 */
100 this.set = function( keys, value ) {
101 if ( typeof keys === 'object' ) {
102 for ( var key in keys ) {
103 values[key] = keys[key];
104 }
105 } else if ( typeof keys === 'string' && typeof value !== 'undefined' ) {
106 values[keys] = value;
107 }
108 };
109 /**
110 * Gets one or multiple configuration values using a key and an optional fallback or an array of keys
111 */
112 this.get = function( keys, fallback ) {
113 if ( typeof keys === 'object' ) {
114 var result = {};
115 for ( var k = 0; k < keys.length; k++ ) {
116 if ( typeof values[keys[k]] !== 'undefined' ) {
117 result[keys[k]] = values[keys[k]];
118 }
119 }
120 return result;
121 } else if ( typeof values[keys] === 'undefined' ) {
122 return typeof fallback !== 'undefined' ? fallback : null;
123 } else {
124 return values[keys];
125 }
126 };
127 /**
128 * Checks if one or multiple configuration fields exist
129 */
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 ) ) {
134 return false;
135 }
136 }
137 return true;
138 } else {
139 return keys in values;
140 }
141 };
142 } )();
143 /*
144 * Localization system
145 */
146 this.msg = new ( function() {
147
148 /* Private Members */
149
150 var that = this;
151 // List of localized messages
152 var messages = {};
153
154 /* Public Methods */
155
156 this.set = function( keys, value ) {
157 if ( typeof keys === 'object' ) {
158 for ( var key in keys ) {
159 messages[key] = keys[key];
160 }
161 } else if ( typeof keys === 'string' && typeof value !== 'undefined' ) {
162 messages[keys] = value;
163 }
164 };
165 this.get = function( key, args ) {
166 if ( !( key in messages ) ) {
167 return '<' + key + '>';
168 }
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] );
173 }
174 } else if ( typeof args == 'string' || typeof args == 'number' ) {
175 msg = msg.replace( '$1', args );
176 }
177 return msg;
178 };
179 } )();
180 /*
181 * Client-side module loader which integrates with the MediaWiki ResourceLoader
182 */
183 this.loader = new ( function() {
184
185 /* Private Members */
186
187 var that = this;
188 var server = 'load.php';
189 /*
190 * Mapping of registered modules
191 *
192 * Format:
193 * {
194 * 'moduleName': {
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' }
200 * }
201 * }
202 */
203 var registry = {};
204 // List of callbacks waiting on dependent modules to be loaded so they can be executed
205 var queue = [];
206 // Until document ready, load requests will be collected in a batch queue
207 var batch = [];
208 // True after document ready occurs
209 var ready = false;
210
211 /* Private Methods */
212
213 /**
214 * Gets a list of modules names that a module needs in their proper dependency order
215 *
216 * @param string module name
217 * @return
218 * @throws Error if circular reference is detected
219 */
220 function needs( module ) {
221 if ( !( module in registry ) ) {
222 // Undefined modules have no needs
223 return [];
224 }
225 var resolved = [];
226 var unresolved = [];
227 if ( arguments.length === 3 ) {
228 // Use arguemnts on inner call
229 resolved = arguments[1];
230 unresolved = arguments[2];
231 }
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 ) {
236 throw new Error(
237 'Circular reference detected: ' + module + ' -> ' + registry[module].needs[n]
238 );
239 }
240 needs( registry[module].needs[n], resolved, unresolved );
241 }
242 }
243 resolved[resolved.length] = module;
244 unresolved.slice( unresolved.indexOf( module ), 1 );
245 if ( arguments.length === 1 ) {
246 // Return resolved list on outer call
247 return resolved;
248 }
249 };
250 /**
251 * Narrows a list of module names down to those matching a specific state. Possible states are 'undefined',
252 * 'registered', 'loading', 'loaded', or 'ready'
253 *
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
257 */
258 function filter( states, modules ) {
259 var list = [];
260 if ( typeof modules === 'undefined' ) {
261 modules = [];
262 for ( module in registry ) {
263 modules[modules.length] = module;
264 }
265 }
266 for ( var s in states ) {
267 for ( var m in modules ) {
268 if (
269 ( states[s] == 'undefined' && typeof registry[modules[m]] === 'undefined' ) ||
270 ( typeof registry[modules[m]] === 'object' && registry[modules[m]].state === states[s] )
271 ) {
272 list[list.length] = modules[m];
273 }
274 }
275 }
276 return list;
277 }
278 /**
279 * Executes a loaded module, making it ready to use
280 *
281 * @param string module name to execute
282 */
283 function execute( module ) {
284 if ( typeof registry[module] === 'undefined' ) {
285 throw new Error( 'module has not been registered: ' + module );
286 }
287 switch ( registry[module].state ) {
288 case 'registered':
289 throw new Error( 'module has not completed loading: ' + module );
290 break;
291 case 'loading':
292 throw new Error( 'module has not completed loading: ' + module );
293 break;
294 case 'ready':
295 throw new Error( 'module has already been loaded: ' + module );
296 break;
297 }
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>' );
301 }
302 // Add localizations to message system
303 if ( typeof registry[module].localization === 'object' ) {
304 mw.msg.set( registry[module].localization );
305 }
306 // Execute script
307 try {
308 registry[module].script();
309 } catch( e ) {
310 mw.log( 'Exception thrown by ' + module + ': ' + e.message );
311 }
312 // Change state
313 registry[module].state = 'ready';
314
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 ) {
319 execute( r );
320 }
321 }
322 }
323 }
324 /**
325 * Adds a callback and it's needs to the queue
326 *
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
329 */
330 function request( needs, callback ) {
331 queue[queue.length] = { 'needs': filter( ['undefined', 'registered'], needs ), 'callback': callback };
332 }
333
334 /* Public Methods */
335
336 /**
337 * Processes the queue, loading and executing when things when ready.
338 */
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';
350 }
351 }
352 }
353 }
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] ) );
359 }
360 }
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
364 batch.sort();
365
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' }
371 );
372 var requests = [];
373 if ( base.debug == '1' ) {
374 for ( b in batch ) {
375 requests[requests.length] = $.extend( { 'modules': batch[b] }, base );
376 }
377 } else {
378 requests[requests.length] = $.extend( { 'modules': batch.join( '|' ) }, base );
379 }
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
386 batch = [];
387 var html = '';
388 for ( r in requests ) {
389 // Build out the HTML
390 var src = mw.util.buildUrlString( {
391 'path': mw.config.get( 'wgScriptPath' ) + '/load.php',
392 'query': requests[r]
393 } );
394 html += '<script type="text/javascript" src="' + src + '"></script>';
395 }
396 // Append script to head
397 $( 'head' ).append( html );
398 }, 0 )
399 }
400 };
401 /**
402 * Registers a module, letting the system know about it and it's dependencies. loader.js files contain calls
403 * to this function.
404 */
405 this.register = function( name, needs ) {
406 // Validate input
407 if ( typeof name !== 'string' ) {
408 throw new Error( 'name must be a string, not a ' + typeof name );
409 }
410 if ( typeof registry[name] !== 'undefined' ) {
411 throw new Error( 'module already implemeneted: ' + name );
412 }
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' ) {
417 needs = needs();
418 }
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;
425 }
426 };
427 /**
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.
430 */
431 this.implement = function( name, script, style, localization ) {
432 // Automaically register module
433 if ( typeof registry[name] === 'undefined' ) {
434 that.register( name, needs );
435 }
436 // Validate input
437 if ( typeof script !== 'function' ) {
438 throw new Error( 'script must be a function, not a ' + typeof script );
439 }
440 if ( typeof style !== 'undefined' && typeof style !== 'string' ) {
441 throw new Error( 'style must be a string, not a ' + typeof style );
442 }
443 if ( typeof localization !== 'undefined' && typeof localization !== 'object' ) {
444 throw new Error( 'localization must be an object, not a ' + typeof localization );
445 }
446 if ( typeof registry[name] !== 'undefined' && typeof registry[name].script !== 'undefined' ) {
447 throw new Error( 'module already implemeneted: ' + name );
448 }
449 // Mark module as loaded
450 registry[name].state = 'loaded';
451 // Attach components
452 registry[name].script = script;
453 if ( typeof style === 'string' ) {
454 registry[name].style = style;
455 }
456 if ( typeof localization === 'object' ) {
457 registry[name].localization = localization;
458 }
459 // Execute or queue callback
460 if ( filter( ['ready'], registry[name].needs ).length == registry[name].needs.length ) {
461 execute( name );
462 } else {
463 request( registry[name].needs, function() { execute( name ); } );
464 }
465 };
466 /**
467 * Executes a function as soon as one or more required modules are ready
468 *
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
471 */
472 this.using = function( needs, callback ) {
473 // Validate input
474 if ( typeof needs !== 'object' && typeof needs !== 'string' ) {
475 throw new Error( 'needs must be a string or an array, not a ' + typeof needs )
476 }
477 if ( typeof callback !== 'function' ) {
478 throw new Error( 'callback must be a function, not a ' + typeof callback )
479 }
480 if ( typeof needs === 'string' ) {
481 needs = [needs];
482 }
483 // Execute or queue callback
484 if ( filter( ['ready'], needs ).length == needs.length ) {
485 callback();
486 } else {
487 request( needs, callback );
488 }
489 };
490
491 /* Event Bindings */
492
493 $( document ).ready( function() {
494 ready = true;
495 that.work();
496 } );
497 } )();
498 /**
499 * General purpose utilities
500 */
501 this.util = new ( function() {
502
503 /* Private Members */
504
505 var that = this;
506 // Decoded user agent string cache
507 var client = null;
508
509 /* Public Methods */
510
511 /**
512 * Builds a url string from an object containing any of the following components:
513 *
514 * Component Example
515 * scheme "http"
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"
520 *
521 * Results in: "http://www.domain.com/path/to/my/file.html?this=th%C3%A5t#place_on_the_page"
522 *
523 * All arguments to this function are assumed to be URL-encoded already, except for the
524 * query parameter if provided in object form.
525 */
526 this.buildUrlString = function( components ) {
527 var url = '';
528 if ( typeof components.scheme === 'string' ) {
529 url += components.scheme + '://';
530 }
531 if ( typeof components.server === 'string' ) {
532 url += components.server + '/';
533 }
534 if ( typeof components.path === 'string' ) {
535 url += components.path;
536 }
537 if ( typeof components.query === 'string' ) {
538 url += '?' + components.query;
539 } else if ( typeof components.query === 'object' ) {
540 url += '?' + that.buildQueryString( components.query );
541 }
542 if ( typeof components.fragment === 'string' ) {
543 url += '#' + components.fragment;
544 }
545 return url;
546 };
547 /**
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.
552 */
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, '+');
561 };
562 /**
563 * Builds a query string from an object with key and values
564 */
565 this.buildQueryString = function( parameters ) {
566 if ( typeof parameters === 'object' ) {
567 var parts = [];
568 for ( var p in parameters ) {
569 parts[parts.length] = that.encodeUrlComponent( p ) + '=' + that.encodeUrlComponent( parameters[p] );
570 }
571 return parts.join( '&' );
572 }
573 return '';
574 };
575 /**
576 * Returns an object containing information about the browser
577 *
578 * The resulting client object will be in the following format:
579 * {
580 * 'name': 'firefox',
581 * 'layout': 'gecko',
582 * 'os': 'linux'
583 * 'version': '3.5.1',
584 * 'versionBase': '3',
585 * 'versionNumber': 3.5,
586 * }
587 */
588 this.client = function() {
589 // Use the cached version if possible
590 if ( client === null ) {
591
592 /* Configuration */
593
594 // Name of browsers or layout engines we don't recognize
595 var uk = 'unknown';
596 // Generic version digit
597 var x = 'x';
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'],
614 ];
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'
620 ];
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
624 var browserNames = [
625 'camino', 'chrome', 'firefox', 'netscape', 'konqueror', 'lynx', 'msie', 'opera', 'safari', 'ipod',
626 'iphone', 'blackberry', 'ps3'
627 ];
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']];
638
639 /* Methods */
640
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] );
645 }
646 return source;
647 };
648
649 /* Pre-processing */
650
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 );
655 }
656 // Everything will be in lowercase from now on
657 userAgent = userAgent.toLowerCase();
658
659 /* Extraction */
660
661 if ( match = new RegExp( '(' + browserNames.join( '|' ) + ')' ).exec( userAgent ) ) {
662 browser = translate( match[1], browserTranslations );
663 }
664 if ( match = new RegExp( '(' + layoutNames.join( '|' ) + ')' ).exec( userAgent ) ) {
665 layout = translate( match[1], layoutTranslations );
666 }
667 if ( match = new RegExp( '(' + osNames.join( '|' ) + ')' ).exec( navigator.platform.toLowerCase() ) ) {
668 var os = translate( match[1], osTranslations );
669 }
670 if ( match = new RegExp( '(' + versionPrefixes.join( '|' ) + ')' + versionSuffix ).exec( userAgent ) ) {
671 version = match[3];
672 }
673
674 /* Edge Cases -- did I mention about how user agent string lie? */
675
676 // Decode Safari's crazy 400+ version numbers
677 if ( name.match( /safari/ ) && version > 400 ) {
678 version = '2.0';
679 }
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;
683 }
684
685 /* Caching */
686
687 client = {
688 'browser': browser,
689 'layout': layout,
690 'os': os,
691 'version': version,
692 'versionBase': ( version !== x ? new String( version ).substr( 0, 1 ) : x ),
693 'versionNumber': ( parseFloat( version, 10 ) || 0.0 )
694 };
695 }
696 return client;
697 };
698 /**
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.
702 *
703 * A browser map is in the following format:
704 * {
705 * 'ltr': {
706 * // Multiple rules with configurable operators
707 * 'msie': [['>=', 7], ['!=', 9]],
708 * // Blocked entirely
709 * 'iphone': false
710 * },
711 * 'rtl': {
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
715 * 'iphone': false
716 * }
717 * }
718 *
719 * @param map Object of browser support map
720 *
721 * @return Boolean true if browser known or assumed to be supported, false if blacklisted
722 */
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
729 return true;
730 }
731 for ( var condition in browser ) {
732 var op = browser[condition][0];
733 var val = browser[condition][1];
734 if ( val === false ) {
735 return false;
736 } else if ( typeof val == 'string' ) {
737 if ( !( eval( 'client.version' + op + '"' + val + '"' ) ) ) {
738 return false;
739 }
740 } else if ( typeof val == 'number' ) {
741 if ( !( eval( 'client.versionNumber' + op + val ) ) ) {
742 return false;
743 }
744 }
745 }
746 return true;
747 };
748 } )();
749 // Attach to window
750 window.mw = $.extend( 'mw' in window ? window.mw : {}, this );
751 } )();