/* * Core MediaWiki JavaScript Library */ var mw = ( function ( $, undefined ) { "use strict"; /* Private Members */ var hasOwn = Object.prototype.hasOwnProperty, slice = Array.prototype.slice; /* Object constructors */ /** * Map * * Creates an object that can be read from or written to from prototype functions * that allow both single and multiple variables at once. * * @param global boolean Whether to store the values in the global window * object or a exclusively in the object property 'values'. * @return Map */ function Map( global ) { this.values = global === true ? window : {}; return this; } Map.prototype = { /** * Get the value of one or multiple a keys. * * If called with no arguments, all values will be returned. * * @param selection mixed String key or array of keys to get values for. * @param fallback mixed Value to use in case key(s) do not exist (optional). * @return mixed If selection was a string returns the value or null, * If selection was an array, returns an object of key/values (value is null if not found), * If selection was not passed or invalid, will return the 'values' object member (be careful as * objects are always passed by reference in JavaScript!). * @return Values as a string or object, null if invalid/inexistant. */ get: function ( selection, fallback ) { var results, i; if ( $.isArray( selection ) ) { selection = slice.call( selection ); results = {}; for ( i = 0; i < selection.length; i += 1 ) { results[selection[i]] = this.get( selection[i], fallback ); } return results; } if ( typeof selection === 'string' ) { if ( this.values[selection] === undefined ) { if ( fallback !== undefined ) { return fallback; } return null; } return this.values[selection]; } if ( selection === undefined ) { return this.values; } // invalid selection key return null; }, /** * Sets one or multiple key/value pairs. * * @param selection {mixed} String key or array of keys to set values for. * @param value {mixed} Value to set (optional, only in use when key is a string) * @return {Boolean} This returns true on success, false on failure. */ set: function ( selection, value ) { var s; if ( $.isPlainObject( selection ) ) { for ( s in selection ) { this.values[s] = selection[s]; } return true; } if ( typeof selection === 'string' && value !== undefined ) { this.values[selection] = value; return true; } return false; }, /** * Checks if one or multiple keys exist. * * @param selection {mixed} String key or array of keys to check * @return {Boolean} Existence of key(s) */ exists: function ( selection ) { var s; if ( $.isArray( selection ) ) { for ( s = 0; s < selection.length; s += 1 ) { if ( this.values[selection[s]] === undefined ) { return false; } } return true; } return this.values[selection] !== undefined; } }; /** * Message * * Object constructor for messages, * similar to the Message class in MediaWiki PHP. * * @param map Map Instance of mw.Map * @param key String * @param parameters Array * @return Message */ function Message( map, key, parameters ) { this.format = 'plain'; this.map = map; this.key = key; this.parameters = parameters === undefined ? [] : slice.call( parameters ); return this; } Message.prototype = { /** * Simple message parser, does $N replacement and nothing else. * This may be overridden to provide a more complex message parser. * * This function will not be called for nonexistent messages. */ parser: function () { var parameters = this.parameters; return this.map.get( this.key ).replace( /\$(\d+)/g, function ( str, match ) { var index = parseInt( match, 10 ) - 1; return parameters[index] !== undefined ? parameters[index] : '$' + match; } ); }, /** * Appends (does not replace) parameters for replacement to the .parameters property. * * @param parameters Array * @return Message */ params: function ( parameters ) { var i; for ( i = 0; i < parameters.length; i += 1 ) { this.parameters.push( parameters[i] ); } return this; }, /** * Converts message object to it's string form based on the state of format. * * @return string Message as a string in the current form or if key does not exist. */ toString: function () { var text; if ( !this.exists() ) { // Use as text if key does not exist if ( this.format !== 'plain' ) { // format 'escape' and 'parse' need to have the brackets and key html escaped return mw.html.escape( '<' + this.key + '>' ); } return '<' + this.key + '>'; } if ( this.format === 'plain' ) { // @todo FIXME: Although not applicable to core Message, // Plugins like jQueryMsg should be able to distinguish // between 'plain' (only variable replacement and plural/gender) // and actually parsing wikitext to HTML. text = this.parser(); } if ( this.format === 'escaped' ) { text = this.parser(); text = mw.html.escape( text ); } if ( this.format === 'parse' ) { text = this.parser(); } return text; }, /** * Changes format to parse and converts message to string * * @return {string} String form of parsed message */ parse: function () { this.format = 'parse'; return this.toString(); }, /** * Changes format to plain and converts message to string * * @return {string} String form of plain message */ plain: function () { this.format = 'plain'; return this.toString(); }, /** * Changes the format to html escaped and converts message to string * * @return {string} String form of html escaped message */ escaped: function () { this.format = 'escaped'; return this.toString(); }, /** * Checks if message exists * * @return {string} String form of parsed message */ exists: function () { return this.map.exists( this.key ); } }; return { /* Public Members */ /** * Dummy function which in debug mode can be replaced with a function that * emulates console.log in console-less environments. */ log: function () { }, /** * @var constructor Make the Map constructor publicly available. */ Map: Map, /** * @var constructor Make the Message constructor publicly available. */ Message: Message, /** * List of configuration values * * Dummy placeholder. Initiated in startUp module as a new instance of mw.Map(). * If $wgLegacyJavaScriptGlobals is true, this Map will have its values * in the global window object. */ config: null, /** * @var object * * Empty object that plugins can be installed in. */ libs: {}, /* Extension points */ legacy: {}, /** * Localization system */ messages: new Map(), /* Public Methods */ /** * Gets a message object, similar to wfMessage() * * @param key string Key of message to get * @param parameter_1 mixed First argument in a list of variadic arguments, * each a parameter for $N replacement in messages. * @return Message */ message: function ( key, parameter_1 /* [, parameter_2] */ ) { var parameters; // Support variadic arguments if ( parameter_1 !== undefined ) { parameters = slice.call( arguments ); parameters.shift(); } else { parameters = []; } return new Message( mw.messages, key, parameters ); }, /** * Gets a message string, similar to wfMessage() * * @param key string Key of message to get * @param parameters mixed First argument in a list of variadic arguments, * each a parameter for $N replacement in messages. * @return String. */ msg: function ( /* key, parameter_1, parameter_2, .. */ ) { return mw.message.apply( mw.message, arguments ).toString(); }, /** * Client-side module loader which integrates with the MediaWiki ResourceLoader */ loader: ( function () { /* Private Members */ /** * Mapping of registered modules * * The jquery module is pre-registered, because it must have already * been provided for this object to have been built, and in debug mode * jquery would have been provided through a unique loader request, * making it impossible to hold back registration of jquery until after * mediawiki. * * For exact details on support for script, style and messages, look at * mw.loader.implement. * * Format: * { * 'moduleName': { * 'version': ############## (unix timestamp), * 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {} * 'group': 'somegroup', (or) null, * 'source': 'local', 'someforeignwiki', (or) null * 'state': 'registered', 'loading', 'loaded', 'ready', 'error' or 'missing' * 'script': ..., * 'style': ..., * 'messages': { 'key': 'value' }, * } * } */ var registry = {}, /** * Mapping of sources, keyed by source-id, values are objects. * Format: * { * 'sourceId': { * 'loadScript': 'http://foo.bar/w/load.php' * } * } */ sources = {}, // List of modules which will be loaded as when ready batch = [], // List of modules to be loaded queue = [], // List of callback functions waiting for modules to be ready to be called jobs = [], // Selector cache for the marker element. Use getMarker() to get/use the marker! $marker = null; /* Private methods */ function getMarker() { // Cached ? if ( $marker ) { return $marker; } $marker = $( 'meta[name="ResourceLoaderDynamicStyles"]' ); if ( $marker.length ) { return $marker; } mw.log( 'getMarker> No found, inserting dynamically.' ); $marker = $( '' ).attr( 'name', 'ResourceLoaderDynamicStyles' ).appendTo( 'head' ); return $marker; } /** * Create a new style tag and add it to the DOM. * * @param text String: CSS text * @param nextnode mixed: [optional] An Element or jQuery object for an element where * the style tag should be inserted before. Otherwise appended to the . * @return HTMLStyleElement */ function addStyleTag( text, nextnode ) { var s = document.createElement( 'style' ); // Insert into document before setting cssText (bug 33305) if ( nextnode ) { // Must be inserted with native insertBefore, not $.fn.before. // When using jQuery to insert it, like $nextnode.before( s ), // then IE6 will throw "Access is denied" when trying to append // to .cssText later. Some kind of weird security measure. // http://stackoverflow.com/q/12586482/319266 // Works: jsfiddle.net/zJzMy/1 // Fails: jsfiddle.net/uJTQz // Works again: http://jsfiddle.net/Azr4w/ (diff: the next 3 lines) if ( nextnode.jquery ) { nextnode = nextnode.get( 0 ); } nextnode.parentNode.insertBefore( s, nextnode ); } else { document.getElementsByTagName( 'head' )[0].appendChild( s ); } if ( s.styleSheet ) { // IE s.styleSheet.cssText = text; } else { // Other browsers. // (Safari sometimes borks on non-string values, // play safe by casting to a string, just in case.) s.appendChild( document.createTextNode( String( text ) ) ); } return s; } /** * Checks if certain cssText is safe to append to * a stylesheet. * * Right now it only makes sure that cssText containing @import * rules will end up in a new stylesheet (as those only work when * placed at the start of a stylesheet; bug 35562). * This could later be extended to take care of other bugs, such as * the IE cssRules limit - not the same as the IE styleSheets limit). */ function canExpandStylesheetWith( $style, cssText ) { return cssText.indexOf( '@import' ) === -1; } function addEmbeddedCSS( cssText ) { var $style, styleEl; $style = getMarker().prev(); // Re-use